UNPKG

46.9 kBTypeScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3
4import { IChangedArgs, PathExt } from '@jupyterlab/coreutils';
5import {
6 Kernel,
7 KernelMessage,
8 KernelSpec,
9 ServerConnection,
10 Session
11} from '@jupyterlab/services';
12import {
13 ITranslator,
14 nullTranslator,
15 TranslationBundle
16} from '@jupyterlab/translation';
17import { find } from '@lumino/algorithm';
18import { JSONExt, PromiseDelegate, UUID } from '@lumino/coreutils';
19import { IDisposable, IObservableDisposable } from '@lumino/disposable';
20import { ISignal, Signal } from '@lumino/signaling';
21import { Widget } from '@lumino/widgets';
22import * as React from 'react';
23import { Dialog, showDialog } from './dialog';
24
25/**
26 * A context object to manage a widget's kernel session connection.
27 *
28 * #### Notes
29 * The current session connection is `.session`, the current session's kernel
30 * connection is `.session.kernel`. For convenience, we proxy several kernel
31 * connection and session connection signals up to the session context so
32 * that you do not have to manage slots as sessions and kernels change. For
33 * example, to act on whatever the current kernel's iopubMessage signal is
34 * producing, connect to the session context `.iopubMessage` signal.
35 *
36 */
37export interface ISessionContext extends IObservableDisposable {
38 /**
39 * The current session connection.
40 */
41 session: Session.ISessionConnection | null;
42
43 /**
44 * Initialize the session context.
45 *
46 * @returns A promise that resolves with whether to ask the user to select a kernel.
47 *
48 * #### Notes
49 * This includes starting up an initial kernel if needed.
50 */
51 initialize(): Promise<boolean>;
52
53 /**
54 * Whether the session context is ready.
55 */
56 readonly isReady: boolean;
57
58 /**
59 * Whether the session context is terminating.
60 */
61 readonly isTerminating: boolean;
62
63 /**
64 * Whether the session context is restarting.
65 */
66 readonly isRestarting: boolean;
67
68 /**
69 * A promise that is fulfilled when the session context is ready.
70 */
71 readonly ready: Promise<void>;
72
73 /**
74 * A signal emitted when the session connection changes.
75 */
76 readonly sessionChanged: ISignal<
77 this,
78 IChangedArgs<
79 Session.ISessionConnection | null,
80 Session.ISessionConnection | null,
81 'session'
82 >
83 >;
84
85 // Signals proxied from the session connection for convenience.
86
87 /**
88 * A signal emitted when the kernel changes, proxied from the session connection.
89 */
90 readonly kernelChanged: ISignal<
91 this,
92 IChangedArgs<
93 Kernel.IKernelConnection | null,
94 Kernel.IKernelConnection | null,
95 'kernel'
96 >
97 >;
98
99 /**
100 * Signal emitted if the kernel preference changes.
101 */
102 readonly kernelPreferenceChanged: ISignal<
103 this,
104 IChangedArgs<ISessionContext.IKernelPreference>
105 >;
106
107 /**
108 * A signal emitted when the kernel status changes, proxied from the session connection.
109 */
110 readonly statusChanged: ISignal<this, Kernel.Status>;
111
112 /**
113 * A signal emitted when the kernel connection status changes, proxied from the session connection.
114 */
115 readonly connectionStatusChanged: ISignal<this, Kernel.ConnectionStatus>;
116
117 /**
118 * A flag indicating if session is has pending input, proxied from the session connection.
119 */
120 readonly pendingInput: boolean;
121
122 /**
123 * A signal emitted for a kernel messages, proxied from the session connection.
124 */
125 readonly iopubMessage: ISignal<this, KernelMessage.IMessage>;
126
127 /**
128 * A signal emitted for an unhandled kernel message, proxied from the session connection.
129 */
130 readonly unhandledMessage: ISignal<this, KernelMessage.IMessage>;
131
132 /**
133 * A signal emitted when a session property changes, proxied from the session connection.
134 */
135 readonly propertyChanged: ISignal<this, 'path' | 'name' | 'type'>;
136
137 /**
138 * The kernel preference for starting new kernels.
139 */
140 kernelPreference: ISessionContext.IKernelPreference;
141
142 /**
143 * Whether the kernel is "No Kernel" or not.
144 *
145 * #### Notes
146 * As the displayed name is translated, this can be used directly.
147 */
148 readonly hasNoKernel: boolean;
149
150 /**
151 * The sensible display name for the kernel, or translated "No Kernel"
152 *
153 * #### Notes
154 * This is at this level since the underlying kernel connection does not
155 * have access to the kernel spec manager.
156 */
157 readonly kernelDisplayName: string;
158
159 /**
160 * A sensible status to display
161 *
162 * #### Notes
163 * This combines the status and connection status into a single status for the user.
164 */
165 readonly kernelDisplayStatus: ISessionContext.KernelDisplayStatus;
166
167 /**
168 * The session path.
169 *
170 * #### Notes
171 * Typically `.session.path` should be used. This attribute is useful if
172 * there is no current session.
173 */
174 readonly path: string;
175
176 /**
177 * The session type.
178 *
179 * #### Notes
180 * Typically `.session.type` should be used. This attribute is useful if
181 * there is no current session.
182 */
183 readonly type: string;
184
185 /**
186 * The session name.
187 *
188 * #### Notes
189 * Typically `.session.name` should be used. This attribute is useful if
190 * there is no current session.
191 */
192 readonly name: string;
193
194 /**
195 * The previous kernel name.
196 */
197 readonly prevKernelName: string;
198
199 /**
200 * The session manager used by the session.
201 */
202 readonly sessionManager: Session.IManager;
203
204 /**
205 * The kernel spec manager
206 */
207 readonly specsManager: KernelSpec.IManager;
208
209 /**
210 * Starts new Kernel.
211 *
212 * @returns Whether to ask the user to pick a kernel.
213 */
214 startKernel(): Promise<boolean>;
215
216 /**
217 * Restart the current Kernel.
218 *
219 * @returns A promise that resolves when the kernel is restarted.
220 */
221 restartKernel(): Promise<void>;
222
223 /**
224 * Kill the kernel and shutdown the session.
225 *
226 * @returns A promise that resolves when the session is shut down.
227 */
228 shutdown(): Promise<void>;
229
230 /**
231 * Change the kernel associated with the session.
232 *
233 * @param options The optional kernel model parameters to use for the new kernel.
234 *
235 * @returns A promise that resolves with the new kernel connection.
236 */
237 changeKernel(
238 options?: Partial<Kernel.IModel>
239 ): Promise<Kernel.IKernelConnection | null>;
240}
241
242/**
243 * The namespace for session context related interfaces.
244 */
245export namespace ISessionContext {
246 /**
247 * A kernel preference.
248 *
249 * #### Notes
250 * Preferences for a kernel are considered in the order `id`, `name`,
251 * `language`. If no matching kernels can be found and `autoStartDefault` is
252 * `true`, then the default kernel for the server is preferred.
253 */
254 export interface IKernelPreference {
255 /**
256 * The name of the kernel.
257 */
258 readonly name?: string;
259
260 /**
261 * The preferred kernel language.
262 */
263 readonly language?: string;
264
265 /**
266 * The id of an existing kernel.
267 */
268 readonly id?: string;
269
270 /**
271 * A kernel should be started automatically (default `true`).
272 */
273 readonly shouldStart?: boolean;
274
275 /**
276 * A kernel can be started (default `true`).
277 */
278 readonly canStart?: boolean;
279
280 /**
281 * Shut down the session when session context is disposed (default `false`).
282 */
283 readonly shutdownOnDispose?: boolean;
284
285 /**
286 * Automatically start the default kernel if no other matching kernel is
287 * found (default `false`).
288 */
289 readonly autoStartDefault?: boolean;
290 }
291
292 export type KernelDisplayStatus =
293 | Kernel.Status
294 | Kernel.ConnectionStatus
295 | 'initializing'
296 | '';
297
298 /**
299 * An interface for a session context dialog provider.
300 */
301 export interface IDialogs {
302 /**
303 * Select a kernel for the session.
304 */
305 selectKernel(session: ISessionContext): Promise<void>;
306
307 /**
308 * Restart the session context.
309 *
310 * @returns A promise that resolves with whether the kernel has restarted.
311 *
312 * #### Notes
313 * If there is a running kernel, present a dialog.
314 * If there is no kernel, we start a kernel with the last run
315 * kernel name and resolves with `true`. If no kernel has been started,
316 * this is a no-op, and resolves with `false`.
317 */
318 restart(session: ISessionContext): Promise<boolean>;
319 }
320
321 /**
322 * Session context dialog options
323 */
324 export interface IDialogsOptions {
325 /**
326 * Application translator object
327 */
328 translator?: ITranslator;
329 }
330}
331
332/**
333 * The default implementation for a session context object.
334 */
335export class SessionContext implements ISessionContext {
336 /**
337 * Construct a new session context.
338 */
339 constructor(options: SessionContext.IOptions) {
340 this.sessionManager = options.sessionManager;
341 this.specsManager = options.specsManager;
342 this.translator = options.translator || nullTranslator;
343 this._trans = this.translator.load('jupyterlab');
344 this._path = options.path ?? UUID.uuid4();
345 this._type = options.type ?? '';
346 this._name = options.name ?? '';
347 this._setBusy = options.setBusy;
348 this._kernelPreference = options.kernelPreference ?? {};
349 }
350
351 /**
352 * The current session connection.
353 */
354 get session(): Session.ISessionConnection | null {
355 return this._session ?? null;
356 }
357
358 /**
359 * The session path.
360 *
361 * #### Notes
362 * Typically `.session.path` should be used. This attribute is useful if
363 * there is no current session.
364 */
365 get path(): string {
366 return this._path;
367 }
368
369 /**
370 * The session type.
371 *
372 * #### Notes
373 * Typically `.session.type` should be used. This attribute is useful if
374 * there is no current session.
375 */
376 get type(): string {
377 return this._type;
378 }
379
380 /**
381 * The session name.
382 *
383 * #### Notes
384 * Typically `.session.name` should be used. This attribute is useful if
385 * there is no current session.
386 */
387 get name(): string {
388 return this._name;
389 }
390
391 /**
392 * A signal emitted when the kernel connection changes, proxied from the session connection.
393 */
394 get kernelChanged(): ISignal<
395 this,
396 Session.ISessionConnection.IKernelChangedArgs
397 > {
398 return this._kernelChanged;
399 }
400
401 /**
402 * A signal emitted when the session connection changes.
403 */
404 get sessionChanged(): ISignal<
405 this,
406 IChangedArgs<
407 Session.ISessionConnection | null,
408 Session.ISessionConnection | null,
409 'session'
410 >
411 > {
412 return this._sessionChanged;
413 }
414
415 /**
416 * A signal emitted when the kernel status changes, proxied from the kernel.
417 */
418 get statusChanged(): ISignal<this, Kernel.Status> {
419 return this._statusChanged;
420 }
421
422 /**
423 * A flag indicating if the session has pending input, proxied from the kernel.
424 */
425 get pendingInput(): boolean {
426 return this._pendingInput;
427 }
428
429 /**
430 * A signal emitted when the kernel status changes, proxied from the kernel.
431 */
432 get connectionStatusChanged(): ISignal<this, Kernel.ConnectionStatus> {
433 return this._connectionStatusChanged;
434 }
435
436 /**
437 * A signal emitted for iopub kernel messages, proxied from the kernel.
438 */
439 get iopubMessage(): ISignal<this, KernelMessage.IIOPubMessage> {
440 return this._iopubMessage;
441 }
442
443 /**
444 * A signal emitted for an unhandled kernel message, proxied from the kernel.
445 */
446 get unhandledMessage(): ISignal<this, KernelMessage.IMessage> {
447 return this._unhandledMessage;
448 }
449
450 /**
451 * A signal emitted when a session property changes, proxied from the current session.
452 */
453 get propertyChanged(): ISignal<this, 'path' | 'name' | 'type'> {
454 return this._propertyChanged;
455 }
456
457 /**
458 * The kernel preference of this client session.
459 *
460 * This is used when selecting a new kernel, and should reflect the sort of
461 * kernel the activity prefers.
462 */
463 get kernelPreference(): ISessionContext.IKernelPreference {
464 return this._kernelPreference;
465 }
466 set kernelPreference(value: ISessionContext.IKernelPreference) {
467 if (!JSONExt.deepEqual(value as any, this._kernelPreference as any)) {
468 const oldValue = this._kernelPreference;
469 this._kernelPreference = value;
470 this._preferenceChanged.emit({
471 name: 'kernelPreference',
472 oldValue,
473 newValue: JSONExt.deepCopy(value as any)
474 });
475 }
476 }
477
478 /**
479 * Signal emitted if the kernel preference changes.
480 */
481 get kernelPreferenceChanged(): ISignal<
482 this,
483 IChangedArgs<ISessionContext.IKernelPreference>
484 > {
485 return this._preferenceChanged;
486 }
487
488 /**
489 * Whether the context is ready.
490 */
491 get isReady(): boolean {
492 return this._isReady;
493 }
494
495 /**
496 * A promise that is fulfilled when the context is ready.
497 */
498 get ready(): Promise<void> {
499 return this._ready.promise;
500 }
501
502 /**
503 * Whether the context is terminating.
504 */
505 get isTerminating(): boolean {
506 return this._isTerminating;
507 }
508
509 /**
510 * Whether the context is restarting.
511 */
512 get isRestarting(): boolean {
513 return this._isRestarting;
514 }
515
516 /**
517 * The session manager used by the session.
518 */
519 readonly sessionManager: Session.IManager;
520
521 /**
522 * The kernel spec manager
523 */
524 readonly specsManager: KernelSpec.IManager;
525
526 /**
527 * Whether the kernel is "No Kernel" or not.
528 *
529 * #### Notes
530 * As the displayed name is translated, this can be used directly.
531 */
532 get hasNoKernel(): boolean {
533 return this.kernelDisplayName === this.noKernelName;
534 }
535
536 /**
537 * The display name of the current kernel, or a sensible alternative.
538 *
539 * #### Notes
540 * This is a convenience function to have a consistent sensible name for the
541 * kernel.
542 */
543 get kernelDisplayName(): string {
544 const kernel = this.session?.kernel;
545 if (this._pendingKernelName === this.noKernelName) {
546 return this.noKernelName;
547 }
548
549 if (this._pendingKernelName) {
550 return (
551 this.specsManager.specs?.kernelspecs[this._pendingKernelName]
552 ?.display_name ?? this._pendingKernelName
553 );
554 }
555 if (!kernel) {
556 return this.noKernelName;
557 }
558 return (
559 this.specsManager.specs?.kernelspecs[kernel.name]?.display_name ??
560 kernel.name
561 );
562 }
563
564 /**
565 * A sensible status to display
566 *
567 * #### Notes
568 * This combines the status and connection status into a single status for
569 * the user.
570 */
571 get kernelDisplayStatus(): ISessionContext.KernelDisplayStatus {
572 const kernel = this.session?.kernel;
573
574 if (this._isTerminating) {
575 return 'terminating';
576 }
577
578 if (this._isRestarting) {
579 return 'restarting';
580 }
581
582 if (this._pendingKernelName === this.noKernelName) {
583 return 'unknown';
584 }
585
586 if (!kernel && this._pendingKernelName) {
587 return 'initializing';
588 }
589
590 if (
591 !kernel &&
592 !this.isReady &&
593 this.kernelPreference.canStart !== false &&
594 this.kernelPreference.shouldStart !== false
595 ) {
596 return 'initializing';
597 }
598
599 return (
600 (kernel?.connectionStatus === 'connected'
601 ? kernel?.status
602 : kernel?.connectionStatus) ?? 'unknown'
603 );
604 }
605
606 /**
607 * The name of the previously started kernel.
608 */
609 get prevKernelName(): string {
610 return this._prevKernelName;
611 }
612
613 /**
614 * Test whether the context is disposed.
615 */
616 get isDisposed(): boolean {
617 return this._isDisposed;
618 }
619
620 /**
621 * A signal emitted when the poll is disposed.
622 */
623 get disposed(): ISignal<this, void> {
624 return this._disposed;
625 }
626
627 /**
628 * Get the constant displayed name for "No Kernel"
629 */
630 protected get noKernelName(): string {
631 return this._trans.__('No Kernel');
632 }
633
634 /**
635 * Dispose of the resources held by the context.
636 */
637 dispose(): void {
638 if (this._isDisposed) {
639 return;
640 }
641 this._isDisposed = true;
642 this._disposed.emit();
643
644 if (this._session) {
645 if (this.kernelPreference.shutdownOnDispose) {
646 // Fire and forget the session shutdown request
647 this.sessionManager.shutdown(this._session.id).catch(reason => {
648 console.error(`Kernel not shut down ${reason}`);
649 });
650 }
651
652 // Dispose the session connection
653 this._session.dispose();
654 this._session = null;
655 }
656 if (this._dialog) {
657 this._dialog.dispose();
658 }
659 if (this._busyDisposable) {
660 this._busyDisposable.dispose();
661 this._busyDisposable = null;
662 }
663 Signal.clearData(this);
664 }
665
666 /**
667 * Starts new Kernel.
668 *
669 * @returns Whether to ask the user to pick a kernel.
670 */
671 async startKernel(): Promise<boolean> {
672 const preference = this.kernelPreference;
673
674 if (!preference.autoStartDefault && preference.shouldStart === false) {
675 return true;
676 }
677
678 let options: Partial<Kernel.IModel> | undefined;
679 if (preference.id) {
680 options = { id: preference.id };
681 } else {
682 const name = Private.getDefaultKernel({
683 specs: this.specsManager.specs,
684 sessions: this.sessionManager.running(),
685 preference
686 });
687 if (name) {
688 options = { name };
689 }
690 }
691
692 if (options) {
693 try {
694 await this._changeKernel(options);
695 return false;
696 } catch (err) {
697 /* no-op */
698 }
699 }
700
701 // Always fall back to selecting a kernel
702 return true;
703 }
704
705 /**
706 * Restart the current Kernel.
707 *
708 * @returns A promise that resolves when the kernel is restarted.
709 */
710 async restartKernel(): Promise<void> {
711 const kernel = this.session?.kernel || null;
712 if (this._isRestarting) {
713 return;
714 }
715 this._isRestarting = true;
716 this._isReady = false;
717 this._statusChanged.emit('restarting');
718 try {
719 await this.session?.kernel?.restart();
720 this._isReady = true;
721 } catch (e) {
722 console.error(e);
723 }
724 this._isRestarting = false;
725 this._statusChanged.emit(this.session?.kernel?.status || 'unknown');
726 this._kernelChanged.emit({
727 name: 'kernel',
728 oldValue: kernel,
729 newValue: this.session?.kernel || null
730 });
731 }
732
733 /**
734 * Change the current kernel associated with the session.
735 */
736 async changeKernel(
737 options: Partial<Kernel.IModel> = {}
738 ): Promise<Kernel.IKernelConnection | null> {
739 if (this.isDisposed) {
740 throw new Error('Disposed');
741 }
742 // Wait for the initialization method to try
743 // and start its kernel first to ensure consistent
744 // ordering.
745 await this._initStarted.promise;
746 return this._changeKernel(options);
747 }
748
749 /**
750 * Kill the kernel and shutdown the session.
751 *
752 * @returns A promise that resolves when the session is shut down.
753 */
754 async shutdown(): Promise<void> {
755 if (this.isDisposed || !this._initializing) {
756 return;
757 }
758 await this._initStarted.promise;
759 this._pendingSessionRequest = '';
760 this._pendingKernelName = this.noKernelName;
761 return this._shutdownSession();
762 }
763
764 /**
765 * Initialize the session context
766 *
767 * @returns A promise that resolves with whether to ask the user to select a kernel.
768 *
769 * #### Notes
770 * If a server session exists on the current path, we will connect to it.
771 * If preferences include disabling `canStart` or `shouldStart`, no
772 * server session will be started.
773 * If a kernel id is given, we attempt to start a session with that id.
774 * If a default kernel is available, we connect to it.
775 * Otherwise we ask the user to select a kernel.
776 */
777 async initialize(): Promise<boolean> {
778 if (this._initializing) {
779 return this._initPromise.promise;
780 }
781 this._initializing = true;
782 const needsSelection = await this._initialize();
783 if (!needsSelection) {
784 this._isReady = true;
785 this._ready.resolve(undefined);
786 }
787 if (!this._pendingSessionRequest) {
788 this._initStarted.resolve(void 0);
789 }
790 this._initPromise.resolve(needsSelection);
791 return needsSelection;
792 }
793
794 /**
795 * Inner initialize function that doesn't handle promises.
796 * This makes it easier to consolidate promise handling logic.
797 */
798 async _initialize(): Promise<boolean> {
799 const manager = this.sessionManager;
800 await manager.ready;
801 await manager.refreshRunning();
802 const model = find(manager.running(), item => {
803 return item.path === this._path;
804 });
805 if (model) {
806 try {
807 const session = manager.connectTo({ model });
808 this._handleNewSession(session);
809 } catch (err) {
810 void this._handleSessionError(err);
811 return Promise.reject(err);
812 }
813 }
814
815 return await this._startIfNecessary();
816 }
817
818 /**
819 * Shut down the current session.
820 */
821 private async _shutdownSession(): Promise<void> {
822 const session = this._session;
823 // Capture starting values in case an error is raised.
824 const isTerminating = this._isTerminating;
825 const isReady = this._isReady;
826 this._isTerminating = true;
827 this._isReady = false;
828 this._statusChanged.emit('terminating');
829 try {
830 await session?.shutdown();
831 this._isTerminating = false;
832 session?.dispose();
833 this._session = null;
834 const kernel = session?.kernel || null;
835 this._statusChanged.emit('unknown');
836 this._kernelChanged.emit({
837 name: 'kernel',
838 oldValue: kernel,
839 newValue: null
840 });
841 this._sessionChanged.emit({
842 name: 'session',
843 oldValue: session,
844 newValue: null
845 });
846 } catch (err) {
847 this._isTerminating = isTerminating;
848 this._isReady = isReady;
849 const status = session?.kernel?.status;
850 if (status === undefined) {
851 this._statusChanged.emit('unknown');
852 } else {
853 this._statusChanged.emit(status);
854 }
855 throw err;
856 }
857 return;
858 }
859
860 /**
861 * Start the session if necessary.
862 *
863 * @returns Whether to ask the user to pick a kernel.
864 */
865 private async _startIfNecessary(): Promise<boolean> {
866 const preference = this.kernelPreference;
867 if (
868 this.isDisposed ||
869 this.session?.kernel ||
870 preference.shouldStart === false ||
871 preference.canStart === false
872 ) {
873 // Not necessary to start a kernel
874 return false;
875 }
876
877 return this.startKernel();
878 }
879
880 /**
881 * Change the kernel.
882 */
883 private async _changeKernel(
884 model: Partial<Kernel.IModel> = {}
885 ): Promise<Kernel.IKernelConnection | null> {
886 if (model.name) {
887 this._pendingKernelName = model.name;
888 }
889
890 if (!this._session) {
891 this._kernelChanged.emit({
892 name: 'kernel',
893 oldValue: null,
894 newValue: null
895 });
896 }
897
898 // Guarantee that the initialized kernel
899 // will be started first.
900 if (!this._pendingSessionRequest) {
901 this._initStarted.resolve(void 0);
902 }
903
904 // If we already have a session, just change the kernel.
905 if (this._session && !this._isTerminating) {
906 try {
907 await this._session.changeKernel(model);
908 return this._session.kernel;
909 } catch (err) {
910 void this._handleSessionError(err);
911 throw err;
912 }
913 }
914
915 // Use a UUID for the path to overcome a race condition on the server
916 // where it will re-use a session for a given path but only after
917 // the kernel finishes starting.
918 // We later switch to the real path below.
919 // Use the correct directory so the kernel will be started in that directory.
920 const dirName = PathExt.dirname(this._path);
921 const requestId = (this._pendingSessionRequest = PathExt.join(
922 dirName,
923 UUID.uuid4()
924 ));
925 try {
926 this._statusChanged.emit('starting');
927 const session = await this.sessionManager.startNew({
928 path: requestId,
929 type: this._type,
930 name: this._name,
931 kernel: model
932 });
933 // Handle a preempt.
934 if (this._pendingSessionRequest !== session.path) {
935 await session.shutdown();
936 session.dispose();
937 return null;
938 }
939 // Change to the real path.
940 await session.setPath(this._path);
941
942 // Update the name in case it has changed since we launched the session.
943 await session.setName(this._name);
944
945 if (this._session && !this._isTerminating) {
946 await this._shutdownSession();
947 }
948 return this._handleNewSession(session);
949 } catch (err) {
950 void this._handleSessionError(err);
951 throw err;
952 }
953 }
954
955 /**
956 * Handle a new session object.
957 */
958 private _handleNewSession(
959 session: Session.ISessionConnection | null
960 ): Kernel.IKernelConnection | null {
961 if (this.isDisposed) {
962 throw Error('Disposed');
963 }
964 if (!this._isReady) {
965 this._isReady = true;
966 this._ready.resolve(undefined);
967 }
968 if (this._session) {
969 this._session.dispose();
970 }
971 this._session = session;
972 this._pendingKernelName = '';
973
974 if (session) {
975 this._prevKernelName = session.kernel?.name ?? '';
976
977 session.disposed.connect(this._onSessionDisposed, this);
978 session.propertyChanged.connect(this._onPropertyChanged, this);
979 session.kernelChanged.connect(this._onKernelChanged, this);
980 session.statusChanged.connect(this._onStatusChanged, this);
981 session.connectionStatusChanged.connect(
982 this._onConnectionStatusChanged,
983 this
984 );
985 session.pendingInput.connect(this._onPendingInput, this);
986 session.iopubMessage.connect(this._onIopubMessage, this);
987 session.unhandledMessage.connect(this._onUnhandledMessage, this);
988
989 if (session.path !== this._path) {
990 this._onPropertyChanged(session, 'path');
991 }
992 if (session.name !== this._name) {
993 this._onPropertyChanged(session, 'name');
994 }
995 if (session.type !== this._type) {
996 this._onPropertyChanged(session, 'type');
997 }
998 }
999
1000 // Any existing session/kernel connection was disposed above when the session was
1001 // disposed, so the oldValue should be null.
1002 this._sessionChanged.emit({
1003 name: 'session',
1004 oldValue: null,
1005 newValue: session
1006 });
1007 this._kernelChanged.emit({
1008 oldValue: null,
1009 newValue: session?.kernel || null,
1010 name: 'kernel'
1011 });
1012 this._statusChanged.emit(session?.kernel?.status || 'unknown');
1013
1014 return session?.kernel || null;
1015 }
1016
1017 /**
1018 * Handle an error in session startup.
1019 */
1020 private async _handleSessionError(
1021 err: ServerConnection.ResponseError
1022 ): Promise<void> {
1023 this._handleNewSession(null);
1024 let traceback = '';
1025 let message = '';
1026 try {
1027 traceback = err.traceback;
1028 message = err.message;
1029 } catch (err) {
1030 // no-op
1031 }
1032 await this._displayKernelError(message, traceback);
1033 }
1034
1035 /**
1036 * Display kernel error
1037 */
1038 private async _displayKernelError(message: string, traceback: string) {
1039 const body = (
1040 <div>
1041 {message && <pre>{message}</pre>}
1042 {traceback && (
1043 <details className="jp-mod-wide">
1044 <pre>{traceback}</pre>
1045 </details>
1046 )}
1047 </div>
1048 );
1049
1050 const dialog = (this._dialog = new Dialog({
1051 title: this._trans.__('Error Starting Kernel'),
1052 body,
1053 buttons: [Dialog.okButton()]
1054 }));
1055 await dialog.launch();
1056 this._dialog = null;
1057 }
1058
1059 /**
1060 * Handle a session termination.
1061 */
1062 private _onSessionDisposed(): void {
1063 if (this._session) {
1064 const oldValue = this._session;
1065 this._session = null;
1066 const newValue = this._session;
1067 this._sessionChanged.emit({ name: 'session', oldValue, newValue });
1068 }
1069 }
1070
1071 /**
1072 * Handle a change to a session property.
1073 */
1074 private _onPropertyChanged(
1075 sender: Session.ISessionConnection,
1076 property: 'path' | 'name' | 'type'
1077 ) {
1078 switch (property) {
1079 case 'path':
1080 this._path = sender.path;
1081 break;
1082 case 'name':
1083 this._name = sender.name;
1084 break;
1085 case 'type':
1086 this._type = sender.type;
1087 break;
1088 default:
1089 throw new Error(`unrecognized property ${property}`);
1090 }
1091 this._propertyChanged.emit(property);
1092 }
1093
1094 /**
1095 * Handle a change to the kernel.
1096 */
1097 private _onKernelChanged(
1098 sender: Session.ISessionConnection,
1099 args: Session.ISessionConnection.IKernelChangedArgs
1100 ): void {
1101 this._kernelChanged.emit(args);
1102 }
1103
1104 /**
1105 * Handle a change to the session status.
1106 */
1107 private _onStatusChanged(
1108 sender: Session.ISessionConnection,
1109 status: Kernel.Status
1110 ): void {
1111 if (status === 'dead') {
1112 const model = sender.kernel?.model;
1113 if (model?.reason) {
1114 const traceback = (model as any).traceback || '';
1115 void this._displayKernelError(model.reason, traceback);
1116 }
1117 }
1118
1119 // Set that this kernel is busy, if we haven't already
1120 // If we have already, and now we aren't busy, dispose
1121 // of the busy disposable.
1122 if (this._setBusy) {
1123 if (status === 'busy') {
1124 if (!this._busyDisposable) {
1125 this._busyDisposable = this._setBusy();
1126 }
1127 } else {
1128 if (this._busyDisposable) {
1129 this._busyDisposable.dispose();
1130 this._busyDisposable = null;
1131 }
1132 }
1133 }
1134
1135 // Proxy the signal
1136 this._statusChanged.emit(status);
1137 }
1138
1139 /**
1140 * Handle a change to the session status.
1141 */
1142 private _onConnectionStatusChanged(
1143 sender: Session.ISessionConnection,
1144 status: Kernel.ConnectionStatus
1145 ): void {
1146 // Proxy the signal
1147 this._connectionStatusChanged.emit(status);
1148 }
1149
1150 /**
1151 * Handle a change to the pending input.
1152 */
1153 private _onPendingInput(
1154 sender: Session.ISessionConnection,
1155 value: boolean
1156 ): void {
1157 // Set the signal value
1158 this._pendingInput = value;
1159 }
1160
1161 /**
1162 * Handle an iopub message.
1163 */
1164 private _onIopubMessage(
1165 sender: Session.ISessionConnection,
1166 message: KernelMessage.IIOPubMessage
1167 ): void {
1168 if (message.header.msg_type === 'shutdown_reply') {
1169 this.session!.kernel!.removeInputGuard();
1170 }
1171 this._iopubMessage.emit(message);
1172 }
1173
1174 /**
1175 * Handle an unhandled message.
1176 */
1177 private _onUnhandledMessage(
1178 sender: Session.ISessionConnection,
1179 message: KernelMessage.IMessage
1180 ): void {
1181 this._unhandledMessage.emit(message);
1182 }
1183
1184 private _path = '';
1185 private _name = '';
1186 private _type = '';
1187 private _prevKernelName: string = '';
1188 private _kernelPreference: ISessionContext.IKernelPreference;
1189 private _isDisposed = false;
1190 private _disposed = new Signal<this, void>(this);
1191 private _session: Session.ISessionConnection | null = null;
1192 private _ready = new PromiseDelegate<void>();
1193 private _initializing = false;
1194 private _initStarted = new PromiseDelegate<void>();
1195 private _initPromise = new PromiseDelegate<boolean>();
1196 private _isReady = false;
1197 private _isTerminating = false;
1198 private _isRestarting = false;
1199 private _kernelChanged = new Signal<
1200 this,
1201 Session.ISessionConnection.IKernelChangedArgs
1202 >(this);
1203 private _preferenceChanged = new Signal<
1204 this,
1205 IChangedArgs<ISessionContext.IKernelPreference>
1206 >(this);
1207 private _sessionChanged = new Signal<
1208 this,
1209 IChangedArgs<
1210 Session.ISessionConnection | null,
1211 Session.ISessionConnection | null,
1212 'session'
1213 >
1214 >(this);
1215 private _statusChanged = new Signal<this, Kernel.Status>(this);
1216 private _connectionStatusChanged = new Signal<this, Kernel.ConnectionStatus>(
1217 this
1218 );
1219 private translator: ITranslator;
1220 private _trans: TranslationBundle;
1221 private _pendingInput = false;
1222 private _iopubMessage = new Signal<this, KernelMessage.IIOPubMessage>(this);
1223 private _unhandledMessage = new Signal<this, KernelMessage.IMessage>(this);
1224 private _propertyChanged = new Signal<this, 'path' | 'name' | 'type'>(this);
1225 private _dialog: Dialog<any> | null = null;
1226 private _setBusy: (() => IDisposable) | undefined;
1227 private _busyDisposable: IDisposable | null = null;
1228 private _pendingKernelName = '';
1229 private _pendingSessionRequest = '';
1230}
1231
1232/**
1233 * A namespace for `SessionContext` statics.
1234 */
1235export namespace SessionContext {
1236 /**
1237 * The options used to initialize a context.
1238 */
1239 export interface IOptions {
1240 /**
1241 * A session manager instance.
1242 */
1243 sessionManager: Session.IManager;
1244
1245 /**
1246 * A kernel spec manager instance.
1247 */
1248 specsManager: KernelSpec.IManager;
1249
1250 /**
1251 * The initial path of the file.
1252 */
1253 path?: string;
1254
1255 /**
1256 * The name of the session.
1257 */
1258 name?: string;
1259
1260 /**
1261 * The type of the session.
1262 */
1263 type?: string;
1264
1265 /**
1266 * A kernel preference.
1267 */
1268 kernelPreference?: ISessionContext.IKernelPreference;
1269
1270 /**
1271 * The application language translator.
1272 */
1273 translator?: ITranslator;
1274
1275 /**
1276 * A function to call when the session becomes busy.
1277 */
1278 setBusy?: () => IDisposable;
1279 }
1280
1281 /**
1282 * An interface for populating a kernel selector.
1283 */
1284 export interface IKernelSearch {
1285 /**
1286 * The Kernel specs.
1287 */
1288 specs: KernelSpec.ISpecModels | null;
1289
1290 /**
1291 * The kernel preference.
1292 */
1293 preference: ISessionContext.IKernelPreference;
1294
1295 /**
1296 * The current running sessions.
1297 */
1298 sessions?: Iterable<Session.IModel>;
1299 }
1300
1301 /**
1302 * Get the default kernel name given select options.
1303 */
1304 export function getDefaultKernel(options: IKernelSearch): string | null {
1305 const { preference } = options;
1306 const { shouldStart } = preference;
1307
1308 if (shouldStart === false) {
1309 return null;
1310 }
1311
1312 return Private.getDefaultKernel(options);
1313 }
1314}
1315
1316/**
1317 * The default implementation of the client session dialog provider.
1318 */
1319export class SessionContextDialogs implements ISessionContext.IDialogs {
1320 constructor(options: ISessionContext.IDialogsOptions = {}) {
1321 this._translator = options.translator ?? nullTranslator;
1322 }
1323
1324 /**
1325 * Select a kernel for the session.
1326 */
1327 async selectKernel(sessionContext: ISessionContext): Promise<void> {
1328 if (sessionContext.isDisposed) {
1329 return Promise.resolve();
1330 }
1331 const trans = this._translator.load('jupyterlab');
1332
1333 // If there is no existing kernel, offer the option
1334 // to keep no kernel.
1335 let label = trans.__('Cancel');
1336 if (sessionContext.hasNoKernel) {
1337 label = sessionContext.kernelDisplayName;
1338 }
1339 const buttons = [
1340 Dialog.cancelButton({
1341 label
1342 }),
1343 Dialog.okButton({
1344 label: trans.__('Select'),
1345 ariaLabel: trans.__('Select Kernel')
1346 })
1347 ];
1348
1349 const autoStartDefault = sessionContext.kernelPreference.autoStartDefault;
1350 const hasCheckbox = typeof autoStartDefault === 'boolean';
1351
1352 const dialog = new Dialog({
1353 title: trans.__('Select Kernel'),
1354 body: new Private.KernelSelector(sessionContext, this._translator),
1355 buttons,
1356 checkbox: hasCheckbox
1357 ? {
1358 label: trans.__('Always start the preferred kernel'),
1359 caption: trans.__(
1360 'Remember my choice and always start the preferred kernel'
1361 ),
1362 checked: autoStartDefault
1363 }
1364 : null
1365 });
1366
1367 const result = await dialog.launch();
1368
1369 if (sessionContext.isDisposed || !result.button.accept) {
1370 return;
1371 }
1372
1373 if (hasCheckbox && result.isChecked !== null) {
1374 sessionContext.kernelPreference = {
1375 ...sessionContext.kernelPreference,
1376 autoStartDefault: result.isChecked
1377 };
1378 }
1379
1380 const model = result.value;
1381 if (model === null && !sessionContext.hasNoKernel) {
1382 return sessionContext.shutdown();
1383 }
1384 if (model) {
1385 await sessionContext.changeKernel(model);
1386 }
1387 }
1388
1389 /**
1390 * Restart the session.
1391 *
1392 * @returns A promise that resolves with whether the kernel has restarted.
1393 *
1394 * #### Notes
1395 * If there is a running kernel, present a dialog.
1396 * If there is no kernel, we start a kernel with the last run
1397 * kernel name and resolves with `true`.
1398 */
1399 async restart(sessionContext: ISessionContext): Promise<boolean> {
1400 const trans = this._translator.load('jupyterlab');
1401
1402 await sessionContext.initialize();
1403 if (sessionContext.isDisposed) {
1404 throw new Error('session already disposed');
1405 }
1406 const kernel = sessionContext.session?.kernel;
1407 if (!kernel && sessionContext.prevKernelName) {
1408 await sessionContext.changeKernel({
1409 name: sessionContext.prevKernelName
1410 });
1411 return true;
1412 }
1413 // Bail if there is no previous kernel to start.
1414 if (!kernel) {
1415 throw new Error('No kernel to restart');
1416 }
1417
1418 const restartBtn = Dialog.warnButton({
1419 label: trans.__('Restart'),
1420 ariaLabel: trans.__('Confirm Kernel Restart')
1421 });
1422 const result = await showDialog({
1423 title: trans.__('Restart Kernel?'),
1424 body: trans.__(
1425 'Do you want to restart the kernel of %1? All variables will be lost.',
1426 sessionContext.name
1427 ),
1428 buttons: [
1429 Dialog.cancelButton({ ariaLabel: trans.__('Cancel Kernel Restart') }),
1430 restartBtn
1431 ]
1432 });
1433
1434 if (kernel.isDisposed) {
1435 return false;
1436 }
1437 if (result.button.accept) {
1438 await sessionContext.restartKernel();
1439 return true;
1440 }
1441 return false;
1442 }
1443
1444 private _translator: ITranslator;
1445}
1446
1447/**
1448 * The namespace for module private data.
1449 */
1450namespace Private {
1451 /**
1452 * A widget that provides a kernel selection.
1453 */
1454 export class KernelSelector extends Widget {
1455 /**
1456 * Create a new kernel selector widget.
1457 */
1458 constructor(sessionContext: ISessionContext, translator?: ITranslator) {
1459 super({ node: createSelectorNode(sessionContext, translator) });
1460 }
1461
1462 /**
1463 * Get the value of the kernel selector widget.
1464 */
1465 getValue(): Kernel.IModel {
1466 const selector = this.node.querySelector('select') as HTMLSelectElement;
1467 return JSON.parse(selector.value) as Kernel.IModel;
1468 }
1469 }
1470
1471 /**
1472 * Create a node for a kernel selector widget.
1473 */
1474 function createSelectorNode(
1475 sessionContext: ISessionContext,
1476 translator?: ITranslator
1477 ) {
1478 // Create the dialog body.
1479 translator = translator || nullTranslator;
1480 const trans = translator.load('jupyterlab');
1481
1482 const body = document.createElement('div');
1483 const text = document.createElement('label');
1484 text.textContent = `${trans.__('Select kernel for:')} "${
1485 sessionContext.name
1486 }"`;
1487 body.appendChild(text);
1488
1489 const options = getKernelSearch(sessionContext);
1490 const selector = document.createElement('select');
1491 populateKernelSelect(
1492 selector,
1493 options,
1494 translator,
1495 !sessionContext.hasNoKernel ? sessionContext.kernelDisplayName : null
1496 );
1497 body.appendChild(selector);
1498 return body;
1499 }
1500
1501 /**
1502 * Get the default kernel name given select options.
1503 */
1504 export function getDefaultKernel(
1505 options: SessionContext.IKernelSearch
1506 ): string | null {
1507 const { specs, preference } = options;
1508 const { name, language, canStart, autoStartDefault } = preference;
1509
1510 if (!specs || canStart === false) {
1511 return null;
1512 }
1513
1514 const defaultName = autoStartDefault ? specs.default : null;
1515
1516 if (!name && !language) {
1517 return defaultName;
1518 }
1519
1520 // Look for an exact match of a spec name.
1521 for (const specName in specs.kernelspecs) {
1522 if (specName === name) {
1523 return name;
1524 }
1525 }
1526
1527 // Bail if there is no language.
1528 if (!language) {
1529 return defaultName;
1530 }
1531
1532 // Check for a single kernel matching the language.
1533 const matches: string[] = [];
1534 for (const specName in specs.kernelspecs) {
1535 const kernelLanguage = specs.kernelspecs[specName]?.language;
1536 if (language === kernelLanguage) {
1537 matches.push(specName);
1538 }
1539 }
1540
1541 if (matches.length === 1) {
1542 const specName = matches[0];
1543 console.warn(
1544 'No exact match found for ' +
1545 specName +
1546 ', using kernel ' +
1547 specName +
1548 ' that matches ' +
1549 'language=' +
1550 language
1551 );
1552 return specName;
1553 }
1554
1555 // No matches found.
1556 return defaultName;
1557 }
1558
1559 /**
1560 * Populate a kernel select node for the session.
1561 */
1562 export function populateKernelSelect(
1563 node: HTMLSelectElement,
1564 options: SessionContext.IKernelSearch,
1565 translator?: ITranslator,
1566 currentKernelDisplayName: string | null = null
1567 ): void {
1568 while (node.firstChild) {
1569 node.removeChild(node.firstChild);
1570 }
1571
1572 const { preference, sessions, specs } = options;
1573 const { name, id, language, canStart, shouldStart } = preference;
1574
1575 translator = translator || nullTranslator;
1576 const trans = translator.load('jupyterlab');
1577
1578 if (!specs || canStart === false) {
1579 node.appendChild(optionForNone(translator));
1580 node.value = 'null';
1581 node.disabled = true;
1582 return;
1583 }
1584
1585 node.disabled = false;
1586
1587 // Create mappings of display names and languages for kernel name.
1588 const displayNames: { [key: string]: string } = Object.create(null);
1589 const languages: { [key: string]: string } = Object.create(null);
1590 for (const name in specs.kernelspecs) {
1591 const spec = specs.kernelspecs[name]!;
1592 displayNames[name] = spec.display_name;
1593 languages[name] = spec.language;
1594 }
1595
1596 // Handle a kernel by name.
1597 const names: string[] = [];
1598 if (name && name in specs.kernelspecs) {
1599 names.push(name);
1600 }
1601
1602 // Then look by language if we have a selected and existing kernel.
1603 if (name && names.length > 0 && language) {
1604 for (const specName in specs.kernelspecs) {
1605 if (name !== specName && languages[specName] === language) {
1606 names.push(specName);
1607 }
1608 }
1609 }
1610
1611 // Use the default kernel if no kernels were found.
1612 if (!names.length) {
1613 names.push(specs.default);
1614 }
1615
1616 // Handle a preferred kernels in order of display name.
1617 const preferred = document.createElement('optgroup');
1618 preferred.label = trans.__('Start Preferred Kernel');
1619
1620 names.sort((a, b) => displayNames[a].localeCompare(displayNames[b]));
1621 for (const name of names) {
1622 preferred.appendChild(optionForName(name, displayNames[name]));
1623 }
1624
1625 if (preferred.firstChild) {
1626 node.appendChild(preferred);
1627 }
1628
1629 // Add an option for no kernel
1630 node.appendChild(optionForNone(translator));
1631
1632 const other = document.createElement('optgroup');
1633 other.label = trans.__('Start Other Kernel');
1634
1635 // Add the rest of the kernel names in alphabetical order.
1636 const otherNames: string[] = [];
1637 for (const specName in specs.kernelspecs) {
1638 if (names.indexOf(specName) !== -1) {
1639 continue;
1640 }
1641 otherNames.push(specName);
1642 }
1643 otherNames.sort((a, b) => displayNames[a].localeCompare(displayNames[b]));
1644 for (const otherName of otherNames) {
1645 other.appendChild(optionForName(otherName, displayNames[otherName]));
1646 }
1647 // Add a separator option if there were any other names.
1648 if (otherNames.length) {
1649 node.appendChild(other);
1650 }
1651
1652 // Handle the default value.
1653 if (shouldStart === false) {
1654 node.value = 'null';
1655 } else {
1656 let selectedIndex = 0;
1657 if (currentKernelDisplayName) {
1658 // Select current kernel by default.
1659 selectedIndex = [...node.options].findIndex(
1660 option => option.text === currentKernelDisplayName
1661 );
1662 selectedIndex = Math.max(selectedIndex, 0);
1663 }
1664 node.selectedIndex = selectedIndex;
1665 }
1666
1667 // Bail if there are no sessions.
1668 if (!sessions) {
1669 return;
1670 }
1671
1672 // Add the sessions using the preferred language first.
1673 const matchingSessions: Session.IModel[] = [];
1674 const otherSessions: Session.IModel[] = [];
1675
1676 for (const session of sessions) {
1677 if (
1678 language &&
1679 session.kernel &&
1680 languages[session.kernel.name] === language &&
1681 session.kernel.id !== id
1682 ) {
1683 matchingSessions.push(session);
1684 } else if (session.kernel?.id !== id) {
1685 otherSessions.push(session);
1686 }
1687 }
1688
1689 const matching = document.createElement('optgroup');
1690 matching.label = trans.__('Use Kernel from Preferred Session');
1691 node.appendChild(matching);
1692
1693 if (matchingSessions.length) {
1694 matchingSessions.sort((a, b) => {
1695 return a.path.localeCompare(b.path);
1696 });
1697
1698 for (const session of matchingSessions) {
1699 const name = session.kernel ? displayNames[session.kernel.name] : '';
1700 matching.appendChild(optionForSession(session, name, translator));
1701 }
1702 }
1703
1704 const otherSessionsNode = document.createElement('optgroup');
1705 otherSessionsNode.label = trans.__('Use Kernel from Other Session');
1706 node.appendChild(otherSessionsNode);
1707
1708 if (otherSessions.length) {
1709 otherSessions.sort((a, b) => {
1710 return a.path.localeCompare(b.path);
1711 });
1712
1713 for (const session of otherSessions) {
1714 const name = session.kernel
1715 ? displayNames[session.kernel.name] || session.kernel.name
1716 : '';
1717 otherSessionsNode.appendChild(
1718 optionForSession(session, name, translator)
1719 );
1720 }
1721 }
1722 }
1723
1724 /**
1725 * Get the kernel search options given a session context and session manager.
1726 */
1727 function getKernelSearch(
1728 sessionContext: ISessionContext
1729 ): SessionContext.IKernelSearch {
1730 return {
1731 specs: sessionContext.specsManager.specs,
1732 sessions: sessionContext.sessionManager.running(),
1733 preference: sessionContext.kernelPreference
1734 };
1735 }
1736
1737 /**
1738 * Create an option element for a kernel name.
1739 */
1740 function optionForName(name: string, displayName: string): HTMLOptionElement {
1741 const option = document.createElement('option');
1742 option.text = displayName;
1743 option.value = JSON.stringify({ name });
1744 return option;
1745 }
1746
1747 /**
1748 * Create an option for no kernel.
1749 */
1750 function optionForNone(translator?: ITranslator): HTMLOptGroupElement {
1751 translator = translator || nullTranslator;
1752 const trans = translator.load('jupyterlab');
1753
1754 const group = document.createElement('optgroup');
1755 group.label = trans.__('Use No Kernel');
1756 const option = document.createElement('option');
1757 option.text = trans.__('No Kernel');
1758 option.value = 'null';
1759 group.appendChild(option);
1760 return group;
1761 }
1762
1763 /**
1764 * Create an option element for a session.
1765 */
1766 function optionForSession(
1767 session: Session.IModel,
1768 displayName: string,
1769 translator?: ITranslator
1770 ): HTMLOptionElement {
1771 translator = translator || nullTranslator;
1772 const trans = translator.load('jupyterlab');
1773
1774 const option = document.createElement('option');
1775 const sessionName = session.name || PathExt.basename(session.path);
1776 option.text = sessionName;
1777 option.value = JSON.stringify({ id: session.kernel?.id });
1778 option.title =
1779 `${trans.__('Path:')} ${session.path}\n` +
1780 `${trans.__('Name:')} ${sessionName}\n` +
1781 `${trans.__('Kernel Name:')} ${displayName}\n` +
1782 `${trans.__('Kernel Id:')} ${session.kernel?.id}`;
1783 return option;
1784 }
1785}