/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import { nb, connection } from 'azdata'; import { localize } from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IClientSession, INotebookModel, IDefaultConnection, INotebookModelOptions, ICellModel, NotebookContentChange, notebookConstants } from './modelInterfaces'; import { NotebookChangeType, CellType } from 'sql/workbench/parts/notebook/models/contracts'; import { nbversion } from '../notebookConstants'; import * as notebookUtils from '../notebookUtils'; import { INotebookManager, SQL_NOTEBOOK_PROVIDER, DEFAULT_NOTEBOOK_PROVIDER } from 'sql/workbench/services/notebook/common/notebookService'; import { NotebookContexts } from 'sql/workbench/parts/notebook/models/notebookContexts'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { INotification, Severity } from 'vs/platform/notification/common/notification'; import { URI } from 'vs/base/common/uri'; import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes'; import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; import { uriPrefixes } from 'sql/platform/connection/common/utils'; import { keys } from 'vs/base/common/map'; /* * Used to control whether a message in a dialog/wizard is displayed as an error, * warning, or informational message. Default is error. */ export enum MessageLevel { Error = 0, Warning = 1, Information = 2 } export class ErrorInfo { constructor(public readonly message: string, public readonly severity: MessageLevel) { } } export class NotebookModel extends Disposable implements INotebookModel { private _contextsChangedEmitter = new Emitter(); private _contextsLoadingEmitter = new Emitter(); private _contentChangedEmitter = new Emitter(); private _kernelsChangedEmitter = new Emitter(); private _kernelChangedEmitter = new Emitter(); private _layoutChanged = new Emitter(); private _inErrorState: boolean = false; private _activeClientSession: IClientSession; private _sessionLoadFinished: Promise; private _onClientSessionReady = new Emitter(); private _onProviderIdChanged = new Emitter(); private _activeContexts: IDefaultConnection; private _trustedMode: boolean; private _cells: ICellModel[]; private _defaultLanguageInfo: nb.ILanguageInfo; private _language: string; private _onErrorEmitter = new Emitter(); private _savedKernelInfo: nb.IKernelInfo; private readonly _nbformat: number = nbversion.MAJOR_VERSION; private readonly _nbformatMinor: number = nbversion.MINOR_VERSION; private _activeConnection: ConnectionProfile; private _otherConnections: ConnectionProfile[] = []; private _activeCell: ICellModel; private _providerId: string; private _defaultKernel: nb.IKernelSpec; private _kernelDisplayNameToConnectionProviderIds: Map = new Map(); private _kernelDisplayNameToNotebookProviderIds: Map = new Map(); private _onValidConnectionSelected = new Emitter(); private _oldKernel: nb.IKernel; private _clientSessionListeners: IDisposable[] = []; private _connectionUrisToDispose: string[] = []; constructor(private _notebookOptions: INotebookModelOptions, startSessionImmediately?: boolean, public connectionProfile?: IConnectionProfile) { super(); if (!_notebookOptions || !_notebookOptions.notebookUri || !_notebookOptions.notebookManagers) { throw new Error('path or notebook service not defined'); } this._trustedMode = false; this._providerId = _notebookOptions.providerId; this._onProviderIdChanged.fire(this._providerId); this._notebookOptions.standardKernels.forEach(kernel => { let displayName = kernel.displayName; if (!displayName) { displayName = kernel.name; } this._kernelDisplayNameToConnectionProviderIds.set(displayName, kernel.connectionProviderIds); this._kernelDisplayNameToNotebookProviderIds.set(displayName, kernel.notebookProvider); }); if (this._notebookOptions.layoutChanged) { this._notebookOptions.layoutChanged(() => this._layoutChanged.fire()); } this._defaultKernel = _notebookOptions.defaultKernel; } public get notebookManagers(): INotebookManager[] { let notebookManagers = this._notebookOptions.notebookManagers.filter(manager => manager.providerId !== DEFAULT_NOTEBOOK_PROVIDER); if (!notebookManagers.length) { return this._notebookOptions.notebookManagers; } return notebookManagers; } public get notebookManager(): INotebookManager { let manager = this.notebookManagers.find(manager => manager.providerId === this._providerId); if (!manager) { // Note: this seems like a less than ideal scenario. We should ideally pass in the "correct" provider ID and allow there to be a default, // instead of assuming in the NotebookModel constructor that the option is either SQL or Jupyter manager = this.notebookManagers.find(manager => manager.providerId === DEFAULT_NOTEBOOK_PROVIDER); } return manager; } public getNotebookManager(providerId: string): INotebookManager { if (providerId) { return this.notebookManagers.find(manager => manager.providerId === providerId); } return undefined; } public get notebookOptions(): INotebookModelOptions { return this._notebookOptions; } public get notebookUri(): URI { return this._notebookOptions.notebookUri; } public set notebookUri(value: URI) { this._notebookOptions.notebookUri = value; } public get hasServerManager(): boolean { // If the service has a server manager, then we can show the start button return !!this.notebookManager.serverManager; } public get contentChanged(): Event { return this._contentChangedEmitter.event; } public get isSessionReady(): boolean { return !!this._activeClientSession; } /** * ClientSession object which handles management of a session instance, * plus startup of the session manager which can return key metadata about the * notebook environment */ public get clientSession(): IClientSession { return this._activeClientSession; } public get kernelChanged(): Event { return this._kernelChangedEmitter.event; } public get kernelsChanged(): Event { return this._kernelsChangedEmitter.event; } public get layoutChanged(): Event { return this._layoutChanged.event; } public get defaultKernel(): nb.IKernelSpec { return this._defaultKernel; } public get contextsChanged(): Event { return this._contextsChangedEmitter.event; } public get contextsLoading(): Event { return this._contextsLoadingEmitter.event; } public get cells(): ICellModel[] { return this._cells; } public get contexts(): IDefaultConnection { return this._activeContexts; } public get specs(): nb.IAllKernels | undefined { let specs: nb.IAllKernels = { defaultKernel: undefined, kernels: [] }; this.notebookManagers.forEach(manager => { if (manager.sessionManager && manager.sessionManager.specs && manager.sessionManager.specs.kernels) { manager.sessionManager.specs.kernels.forEach(kernel => { specs.kernels.push(kernel); }); if (!specs.defaultKernel) { specs.defaultKernel = manager.sessionManager.specs.defaultKernel; } } }); return specs; } public standardKernelsDisplayName(): string[] { return Array.from(keys(this._kernelDisplayNameToNotebookProviderIds)); } public get inErrorState(): boolean { return this._inErrorState; } public get onError(): Event { return this._onErrorEmitter.event; } public get trustedMode(): boolean { return this._trustedMode; } public get providerId(): string { return this._providerId; } public set trustedMode(isTrusted: boolean) { this._trustedMode = isTrusted; if (this._cells) { this._cells.forEach(c => { c.trustedMode = this._trustedMode; }); } } public get activeConnection(): IConnectionProfile { return this._activeConnection; } /** * Indicates the server has finished loading. It may have failed to load in * which case the view will be in an error state. */ public get sessionLoadFinished(): Promise { return this._sessionLoadFinished; } /** * Notifies when the client session is ready for use */ public get onClientSessionReady(): Event { return this._onClientSessionReady.event; } public get onProviderIdChange(): Event { return this._onProviderIdChanged.event; } public get onValidConnectionSelected(): Event { return this._onValidConnectionSelected.event; } public getApplicableConnectionProviderIds(kernelDisplayName: string): string[] { let ids = []; if (kernelDisplayName) { ids = this._kernelDisplayNameToConnectionProviderIds.get(kernelDisplayName); } return !ids ? [] : ids; } public async requestModelLoad(isTrusted: boolean = false): Promise { try { this._trustedMode = isTrusted; let contents = null; if (this._notebookOptions && this._notebookOptions.contentManager) { contents = await this._notebookOptions.contentManager.loadContent(); } let factory = this._notebookOptions.factory; // if cells already exist, create them with language info (if it is saved) this._cells = []; if (contents) { this._defaultLanguageInfo = this.getDefaultLanguageInfo(contents); this._savedKernelInfo = this.getSavedKernelInfo(contents); if (contents.cells && contents.cells.length > 0) { this._cells = contents.cells.map(c => factory.createCell(c, { notebook: this, isTrusted: isTrusted })); } } this.setDefaultKernelAndProviderId(); this.trySetLanguageFromLangInfo(); } catch (error) { this._inErrorState = true; throw error; } } public findCellIndex(cellModel: ICellModel): number { return this._cells.findIndex((cell) => cell.equals(cellModel)); } public addCell(cellType: CellType, index?: number): ICellModel { if (this.inErrorState) { return null; } let cell = this.createCell(cellType); if (index !== undefined && index !== null && index >= 0 && index < this._cells.length) { this._cells.splice(index, 0, cell); } else { this._cells.push(cell); index = undefined; } // Set newly created cell as active cell this.updateActiveCell(cell); this._contentChangedEmitter.fire({ changeType: NotebookChangeType.CellsAdded, cells: [cell], cellIndex: index }); return cell; } private updateActiveCell(cell: ICellModel) { if (this._activeCell) { this._activeCell.active = false; } this._activeCell = cell; this._activeCell.active = true; } private createCell(cellType: CellType): ICellModel { let singleCell: nb.ICellContents = { cell_type: cellType, source: '', metadata: {}, execution_count: undefined }; return this._notebookOptions.factory.createCell(singleCell, { notebook: this, isTrusted: true }); } deleteCell(cellModel: ICellModel): void { if (this.inErrorState || !this._cells) { return; } let index = this._cells.findIndex((cell) => cell.equals(cellModel)); if (index > -1) { this._cells.splice(index, 1); this._contentChangedEmitter.fire({ changeType: NotebookChangeType.CellDeleted, cells: [cellModel], cellIndex: index }); } else { this.notifyError(localize('deleteCellFailed', "Failed to delete cell.")); } } pushEditOperations(edits: ISingleNotebookEditOperation[]): void { if (this.inErrorState || !this._cells) { return; } for (let edit of edits) { let newCells: ICellModel[] = []; if (edit.cell) { // TODO: should we validate and complete required missing parameters? let contents: nb.ICellContents = edit.cell as nb.ICellContents; newCells.push(this._notebookOptions.factory.createCell(contents, { notebook: this, isTrusted: this._trustedMode })); } this._cells.splice(edit.range.start, edit.range.end - edit.range.start, ...newCells); if (newCells.length > 0) { this.updateActiveCell(newCells[0]); } this._contentChangedEmitter.fire({ changeType: NotebookChangeType.CellsAdded }); } } public get activeCell(): ICellModel { return this._activeCell; } public set activeCell(value: ICellModel) { this._activeCell = value; } private notifyError(error: string): void { this._onErrorEmitter.fire({ message: error, severity: Severity.Error }); } public async startSession(manager: INotebookManager, displayName?: string, setErrorStateOnFail?: boolean): Promise { if (displayName) { let standardKernel = this._notebookOptions.standardKernels.find(kernel => kernel.displayName === displayName); this._defaultKernel = displayName ? { name: standardKernel.name, display_name: standardKernel.displayName } : this._defaultKernel; } if (this._defaultKernel) { let clientSession = this._notebookOptions.factory.createClientSession({ notebookUri: this._notebookOptions.notebookUri, notebookManager: manager, notificationService: this._notebookOptions.notificationService, kernelSpec: this._defaultKernel }); if (!this._activeClientSession) { this.updateActiveClientSession(clientSession); } let profile = new ConnectionProfile(this._notebookOptions.capabilitiesService, this.connectionProfile); // TODO: this code needs to be fixed since it is called before the this._savedKernelInfo is set. // This means it always fails, and we end up using the default connection instead. If you right-click // and run "New Notebook" on a disconnected server this means you get the wrong connection (global active) // instead of the one you chose, or it'll fail to connect in general if (this.isValidConnection(profile)) { this._activeConnection = profile; } else { this._activeConnection = undefined; } clientSession.onKernelChanging(async (e) => { await this.loadActiveContexts(e); }); clientSession.statusChanged(async (session) => { this._kernelsChangedEmitter.fire(session.kernel); }); await clientSession.initialize(); // By somehow we have to wait for ready, otherwise may not be called for some cases. await clientSession.ready; if (clientSession.kernel) { await clientSession.kernel.ready; await this.updateKernelInfoOnKernelChange(clientSession.kernel); } if (clientSession.isInErrorState) { if (setErrorStateOnFail) { this.setErrorState(clientSession.errorMessage); } else { throw new Error(clientSession.errorMessage); } } this._onClientSessionReady.fire(clientSession); this._kernelChangedEmitter.fire({ oldValue: undefined, newValue: clientSession.kernel }); } } // When changing kernel, update the active session and register the kernel change event // So KernelDropDown could get the event fired when added listerner on Model.KernelChange private updateActiveClientSession(clientSession: IClientSession) { this.clearClientSessionListeners(); this._activeClientSession = clientSession; this._clientSessionListeners.push(this._activeClientSession.kernelChanged(e => this._kernelChangedEmitter.fire(e))); } private clearClientSessionListeners() { this._clientSessionListeners.forEach(listener => listener.dispose()); this._clientSessionListeners = []; } public setDefaultKernelAndProviderId() { if (this._savedKernelInfo) { this.sanitizeSavedKernelInfo(); let provider = this._kernelDisplayNameToNotebookProviderIds.get(this._savedKernelInfo.display_name); if (provider && provider !== this._providerId) { this._providerId = provider; } this._defaultKernel = this._savedKernelInfo; } else if (this._defaultKernel) { let providerId = this._kernelDisplayNameToNotebookProviderIds.get(this._defaultKernel.display_name); if (providerId) { if (this._providerId !== providerId) { this._providerId = providerId; } } else { this._defaultKernel = notebookConstants.sqlKernelSpec; this._providerId = SQL_NOTEBOOK_PROVIDER; } } else { this._defaultKernel = notebookConstants.sqlKernelSpec; this._providerId = SQL_NOTEBOOK_PROVIDER; } // update default language this._defaultLanguageInfo = { name: this._providerId === SQL_NOTEBOOK_PROVIDER ? 'sql' : 'python', version: '' }; } private isValidConnection(profile: IConnectionProfile | connection.Connection) { let standardKernels = this._notebookOptions.standardKernels.find(kernel => this._defaultKernel && kernel.displayName === this._defaultKernel.display_name); let connectionProviderIds = standardKernels ? standardKernels.connectionProviderIds : undefined; return profile && connectionProviderIds && connectionProviderIds.find(provider => provider === profile.providerName) !== undefined; } public getStandardKernelFromName(name: string): notebookUtils.IStandardKernelWithProvider { if (name) { let kernel = this._notebookOptions.standardKernels.find(kernel => kernel.name.toLowerCase() === name.toLowerCase()); return kernel; } return undefined; } public getStandardKernelFromDisplayName(displayName: string): notebookUtils.IStandardKernelWithProvider { if (displayName) { let kernel = this._notebookOptions.standardKernels.find(kernel => kernel.displayName.toLowerCase() === displayName.toLowerCase()); return kernel; } return undefined; } public get languageInfo(): nb.ILanguageInfo { return this._defaultLanguageInfo; } public get language(): string { return this._language; } private updateLanguageInfo(info: nb.ILanguageInfo) { if (info) { this._defaultLanguageInfo = info; this.trySetLanguageFromLangInfo(); } } private trySetLanguageFromLangInfo() { // In languageInfo, set the language to the "name" property // If the "name" property isn't defined, check the "mimeType" property // Otherwise, default to python as the language let languageInfo = this.languageInfo; let language: string; if (languageInfo) { if (languageInfo.codemirror_mode) { let codeMirrorMode: nb.ICodeMirrorMode = (languageInfo.codemirror_mode); if (codeMirrorMode && codeMirrorMode.name) { language = codeMirrorMode.name; } } if (!language && languageInfo.name) { language = languageInfo.name; } if (!language && languageInfo.mimetype) { language = languageInfo.mimetype; } } if (language) { let mimeTypePrefix = 'x-'; if (language.includes(mimeTypePrefix)) { language = language.replace(mimeTypePrefix, ''); } else if (language.toLowerCase() === 'ipython') { // Special case ipython because in many cases this is defined as the code mirror mode for python notebooks language = 'python'; } } this._language = language; } public changeKernel(displayName: string): void { this._contextsLoadingEmitter.fire(); this.doChangeKernel(displayName, true); } private async doChangeKernel(displayName: string, mustSetProvider: boolean = true, restoreOnFail: boolean = true): Promise { if (!displayName) { // Can't change to an undefined kernel return; } let oldDisplayName = this._activeClientSession && this._activeClientSession.kernel ? this._activeClientSession.kernel.name : undefined; try { let changeKernelNeeded = true; if (mustSetProvider) { let providerChanged = await this.tryStartSessionByChangingProviders(displayName); // If provider was changed, a new session with new kernel is already created. We can skip calling changeKernel. changeKernelNeeded = !providerChanged; } if (changeKernelNeeded) { let spec = this.findSpec(displayName); if (this._activeClientSession && this._activeClientSession.isReady) { let kernel = await this._activeClientSession.changeKernel(spec, this._oldKernel); try { await kernel.ready; await this.updateKernelInfoOnKernelChange(kernel); } catch (err2) { // TODO should we handle this in any way? console.log(`doChangeKernel: ignoring error ${notebookUtils.getErrorMessage(err2)}`); } } } } catch (err) { if (oldDisplayName && restoreOnFail) { this.notifyError(localize('changeKernelFailedRetry', "Failed to change kernel. Kernel {0} will be used. Error was: {1}", oldDisplayName, notebookUtils.getErrorMessage(err))); // Clear out previous kernel let failedProviderId = this.tryFindProviderForKernel(displayName, true); let oldProviderId = this.tryFindProviderForKernel(oldDisplayName, true); if (failedProviderId !== oldProviderId) { // We need to clear out the old kernel information so we switch providers. Otherwise in the SQL -> Jupyter -> SQL failure case, // we would never reset the providers this._oldKernel = undefined; } return this.doChangeKernel(oldDisplayName, mustSetProvider, false); } else { this.notifyError(localize('changeKernelFailed', "Failed to change kernel due to error: {0}", notebookUtils.getErrorMessage(err))); this._kernelChangedEmitter.fire({ newValue: undefined, oldValue: undefined }); } } // Else no need to do anything } private async updateKernelInfoOnKernelChange(kernel: nb.IKernel) { await this.updateKernelInfo(kernel); if (kernel.info) { this.updateLanguageInfo(kernel.info.language_info); } } private findSpec(displayName: string) { let spec = this.getKernelSpecFromDisplayName(displayName); if (spec) { // Ensure that the kernel we try to switch to is a valid kernel; if not, use the default let kernelSpecs = this.getKernelSpecs(); if (kernelSpecs && kernelSpecs.length > 0 && kernelSpecs.findIndex(k => k.display_name === spec.display_name) < 0) { spec = kernelSpecs.find(spec => spec.name === this.notebookManager.sessionManager.specs.defaultKernel); } } else { spec = notebookConstants.sqlKernelSpec; } return spec; } public async changeContext(title: string, newConnection?: ConnectionProfile, hideErrorMessage?: boolean): Promise { try { if (!newConnection) { newConnection = this._activeContexts.otherConnections.find((connection) => connection.title === title); } if ((!newConnection) && (this._activeContexts.defaultConnection.title === title)) { newConnection = this._activeContexts.defaultConnection; } if (newConnection) { if (this._activeConnection && this._activeConnection.id !== newConnection.id) { this._otherConnections.push(this._activeConnection); } this._activeConnection = newConnection; this.refreshConnections(newConnection); this._activeClientSession.updateConnection(newConnection.toIConnectionProfile()).then( result => { //Remove 'Select connection' from 'Attach to' drop-down since its a valid connection this._onValidConnectionSelected.fire(true); }, error => { if (error) { if (!hideErrorMessage) { this.notifyError(notebookUtils.getErrorMessage(error)); } //Selected a wrong connection, Attach to should be defaulted with 'Select connection' this._onValidConnectionSelected.fire(false); } }); } else { this._onValidConnectionSelected.fire(false); } } catch (err) { let msg = notebookUtils.getErrorMessage(err); this.notifyError(localize('changeContextFailed', "Changing context failed: {0}", msg)); } } private refreshConnections(newConnection: ConnectionProfile) { if (this.isValidConnection(newConnection) && this._activeConnection.id !== '-1' && this._activeConnection.id !== this._activeContexts.defaultConnection.id) { // Put the defaultConnection to the head of otherConnections if (this.isValidConnection(this._activeContexts.defaultConnection)) { this._activeContexts.otherConnections = this._activeContexts.otherConnections.filter(conn => conn.id !== this._activeContexts.defaultConnection.id); this._activeContexts.otherConnections.unshift(this._activeContexts.defaultConnection); } // Change the defaultConnection to newConnection this._activeContexts.defaultConnection = newConnection; } } // Get default language if saved in notebook file // Otherwise, default to python private getDefaultLanguageInfo(notebook: nb.INotebookContents): nb.ILanguageInfo { return (notebook && notebook.metadata && notebook.metadata.language_info) ? notebook.metadata.language_info : { name: this._providerId === SQL_NOTEBOOK_PROVIDER ? 'sql' : 'python', version: '', mimetype: this._providerId === SQL_NOTEBOOK_PROVIDER ? 'x-sql' : 'x-python' }; } // Get default kernel info if saved in notebook file private getSavedKernelInfo(notebook: nb.INotebookContents): nb.IKernelInfo { return (notebook && notebook.metadata && notebook.metadata.kernelspec) ? notebook.metadata.kernelspec : undefined; } private getKernelSpecFromDisplayName(displayName: string): nb.IKernelSpec { displayName = this.sanitizeDisplayName(displayName); let kernel: nb.IKernelSpec = this.specs.kernels.find(k => k.display_name.toLowerCase() === displayName.toLowerCase()); if (!kernel) { return undefined; // undefined is handled gracefully in the session to default to the default kernel } else if (!kernel.name) { kernel.name = this.specs.defaultKernel; } return kernel; } private sanitizeSavedKernelInfo() { if (this._savedKernelInfo) { let displayName = this.sanitizeDisplayName(this._savedKernelInfo.display_name); if (this._savedKernelInfo.display_name !== displayName) { this._savedKernelInfo.display_name = displayName; } let standardKernel = this._notebookOptions.standardKernels.find(kernel => kernel.displayName === displayName || displayName.startsWith(kernel.displayName)); if (standardKernel && this._savedKernelInfo.name && this._savedKernelInfo.name !== standardKernel.name) { this._savedKernelInfo.name = standardKernel.name; this._savedKernelInfo.display_name = standardKernel.displayName; } } } public getDisplayNameFromSpecName(kernel: nb.IKernel): string { let specs = this.notebookManager.sessionManager.specs; if (!specs || !specs.kernels) { return kernel.name; } let newKernel = this.notebookManager.sessionManager.specs.kernels.find(k => k.name === kernel.name); let newKernelDisplayName; if (newKernel) { newKernelDisplayName = newKernel.display_name; } return newKernelDisplayName; } public addAttachToConnectionsToBeDisposed(connUri: string) { this._connectionUrisToDispose.push(connUri); } private setErrorState(errMsg: string): void { this._inErrorState = true; let msg = localize('startSessionFailed', "Could not start session: {0}", errMsg); this.notifyError(msg); } public dispose(): void { super.dispose(); this.disconnectAttachToConnections(); this.handleClosed(); } public async handleClosed(): Promise { try { if (this.notebookOptions && this.notebookOptions.connectionService) { if (this._otherConnections) { notebookUtils.asyncForEach(this._otherConnections, async (conn) => { await this.disconnectNotebookConnection(conn); }); this._otherConnections = []; } if (this._activeConnection) { await this.disconnectNotebookConnection(this._activeConnection); this._activeConnection = undefined; } } await this.shutdownActiveSession(); } catch (err) { console.log('An error occurred when closing the notebook: {0}', notebookUtils.getErrorMessage(err)); } } private async shutdownActiveSession() { if (this._activeClientSession) { try { await this._activeClientSession.ready; } catch (err) { this.notifyError(localize('shutdownClientSessionError', "A client session error occurred when closing the notebook: {0}", notebookUtils.getErrorMessage(err))); } await this._activeClientSession.shutdown(); this.clearClientSessionListeners(); this._activeClientSession = undefined; } } private async loadActiveContexts(kernelChangedArgs: nb.IKernelChangedArgs): Promise { if (kernelChangedArgs && kernelChangedArgs.newValue && kernelChangedArgs.newValue.name) { let kernelDisplayName = this.getDisplayNameFromSpecName(kernelChangedArgs.newValue); this._activeContexts = await NotebookContexts.getContextsForKernel(this._notebookOptions.connectionService, this.getApplicableConnectionProviderIds(kernelDisplayName), kernelChangedArgs, this.connectionProfile); this._contextsChangedEmitter.fire(); if (this.contexts.defaultConnection !== undefined && this.contexts.defaultConnection.serverName !== undefined && this.contexts.defaultConnection.title !== undefined) { await this.changeContext(this.contexts.defaultConnection.title, this.contexts.defaultConnection); } } } /** * Sanitizes display name to remove IP address in order to fairly compare kernels * In some notebooks, display name is in the format () * example: PySpark (25.23.32.4) * @param displayName Display Name for the kernel */ public sanitizeDisplayName(displayName: string): string { let name = displayName; if (name) { let index = name.indexOf('('); name = (index > -1) ? name.substr(0, index - 1).trim() : name; } return name; } private async updateKernelInfo(kernel: nb.IKernel): Promise { if (kernel) { try { let spec = await kernel.getSpec(); this._savedKernelInfo = { name: kernel.name, display_name: spec.display_name, language: spec.language }; this.clientSession.configureKernel(this._savedKernelInfo); } catch (err) { // Don't worry about this for now. Just use saved values } } } /** * Set _providerId and start session if it is new provider * @param displayName Kernel dispay name */ private async tryStartSessionByChangingProviders(displayName: string): Promise { if (displayName) { if (this._activeClientSession && this._activeClientSession.isReady) { this._oldKernel = this._activeClientSession.kernel; } let providerId = this.tryFindProviderForKernel(displayName); if (providerId && providerId !== this._providerId) { this._providerId = providerId; this._onProviderIdChanged.fire(this._providerId); await this.shutdownActiveSession(); let manager = this.getNotebookManager(providerId); if (manager) { await this.startSession(manager, displayName, false); } else { throw new Error(localize('ProviderNoManager', "Can't find notebook manager for provider {0}", providerId)); } return true; } } return false; } private tryFindProviderForKernel(displayName: string, alwaysReturnId: boolean = false): string { if (!displayName) { return undefined; } let standardKernel = this.getStandardKernelFromDisplayName(displayName); if (standardKernel) { let providerId = this._kernelDisplayNameToNotebookProviderIds.get(displayName); if (alwaysReturnId || (!this._oldKernel || this._oldKernel.name !== standardKernel.name)) { return providerId; } } return undefined; } // Get kernel specs from current sessionManager private getKernelSpecs(): nb.IKernelSpec[] { if (this.notebookManager && this.notebookManager.sessionManager && this.notebookManager.sessionManager.specs && this.notebookManager.sessionManager.specs.kernels) { return this.notebookManager.sessionManager.specs.kernels; } return []; } // Check for and disconnect from any new connections opened while in the notebook // Note: notebooks should always connect with the connection URI in the following format, // so that connections can be tracked accordingly throughout ADS: // let connectionUri = Utils.generateUri(connection, 'notebook'); private async disconnectNotebookConnection(conn: ConnectionProfile): Promise { if (this.notebookOptions.connectionService.getConnectionUri(conn).includes(uriPrefixes.notebook)) { let uri = this._notebookOptions.connectionService.getConnectionUri(conn); await this.notebookOptions.connectionService.disconnect(uri).catch(e => console.log(e)); } } // Disconnect any connections that were added through the "Add new connection" functionality in the Attach To dropdown private async disconnectAttachToConnections(): Promise { notebookUtils.asyncForEach(this._connectionUrisToDispose, async conn => { await this.notebookOptions.connectionService.disconnect(conn).catch(e => console.log(e)); }); this._connectionUrisToDispose = []; } /** * Serialize the model to JSON. */ toJSON(): nb.INotebookContents { let cells: nb.ICellContents[] = this.cells.map(c => c.toJSON()); let metadata = Object.create(null) as nb.INotebookMetadata; // TODO update language and kernel when these change metadata.kernelspec = this._savedKernelInfo; metadata.language_info = this.languageInfo; return { metadata, nbformat_minor: this._nbformatMinor, nbformat: this._nbformat, cells }; } onCellChange(cell: ICellModel, change: NotebookChangeType): void { let changeInfo: NotebookContentChange = { changeType: change, cells: [cell] }; switch (change) { case NotebookChangeType.CellOutputUpdated: case NotebookChangeType.CellSourceUpdated: changeInfo.changeType = NotebookChangeType.DirtyStateChanged; changeInfo.isDirty = true; break; default: // Do nothing for now } this._contentChangedEmitter.fire(changeInfo); } }