diff --git a/src/client/datascience/datascience.ts b/src/client/datascience/datascience.ts index 1f0199970823..6a4dd7c2177f 100644 --- a/src/client/datascience/datascience.ts +++ b/src/client/datascience/datascience.ts @@ -10,7 +10,16 @@ import { PYTHON_ALLFILES, PYTHON_LANGUAGE } from '../common/constants'; import { ContextKey } from '../common/contextKey'; import '../common/extensions'; import { traceError } from '../common/logger'; -import { BANNER_NAME_DS_SURVEY, GLOBAL_MEMENTO, IConfigurationService, IDisposable, IDisposableRegistry, IExtensionContext, IMemento, IPythonExtensionBanner } from '../common/types'; +import { + BANNER_NAME_DS_SURVEY, + GLOBAL_MEMENTO, + IConfigurationService, + IDisposable, + IDisposableRegistry, + IExtensionContext, + IMemento, + IPythonExtensionBanner +} from '../common/types'; import { waitForPromise } from '../common/utils/async'; import { debounceAsync, swallowExceptions } from '../common/utils/decorators'; import * as localize from '../common/utils/localize'; @@ -22,7 +31,17 @@ import { hasCells } from './cellFactory'; import { Commands, EditorContexts, Settings, Telemetry } from './constants'; import { createRemoteConnectionInfo } from './jupyter/jupyterUtils'; import { KernelSelector, KernelSpecInterpreter } from './jupyter/kernels/kernelSelector'; -import { ICodeWatcher, IConnection, IDataScience, IDataScienceCodeLensProvider, IDataScienceCommandListener, IJupyterKernel, IJupyterKernelSpec, IJupyterSessionManagerFactory, INotebookEditorProvider } from './types'; +import { LiveKernelModel } from './jupyter/kernels/types'; +import { + ICodeWatcher, + IConnection, + IDataScience, + IDataScienceCodeLensProvider, + IDataScienceCommandListener, + IJupyterKernelSpec, + IJupyterSessionManagerFactory, + INotebookEditorProvider +} from './types'; interface ISelectUriQuickPickItem extends vscode.QuickPickItem { newChoice: boolean; @@ -36,7 +55,8 @@ export class DataScience implements IDataScience { private startTime: number = Date.now(); private localLabel = `$(zap) ${localize.DataScience.jupyterSelectURILocalLabel()}`; private newLabel = `$(server) ${localize.DataScience.jupyterSelectURINewLabel()}`; - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, + constructor( + @inject(IServiceContainer) private serviceContainer: IServiceContainer, @inject(ICommandManager) private commandManager: ICommandManager, @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, @inject(IExtensionContext) private extensionContext: IExtensionContext, @@ -62,11 +82,7 @@ export class DataScience implements IDataScience { public async activate(): Promise { this.registerCommands(); - this.extensionContext.subscriptions.push( - vscode.languages.registerCodeLensProvider( - PYTHON_ALLFILES, this.dataScienceCodeLensProvider - ) - ); + this.extensionContext.subscriptions.push(vscode.languages.registerCodeLensProvider(PYTHON_ALLFILES, this.dataScienceCodeLensProvider)); // Set our initial settings and sign up for changes this.onSettingsChanged(); @@ -227,12 +243,12 @@ export class DataScience implements IDataScience { } @captureTelemetry(Telemetry.SelectLocalJupyterKernel) - public async selectLocalJupyterKernel(currentKernel?: IJupyterKernelSpec | IJupyterKernel & Partial): Promise { + public async selectLocalJupyterKernel(currentKernel?: IJupyterKernelSpec | LiveKernelModel): Promise { return this.kernelSelector.selectLocalKernel(undefined, undefined, currentKernel); } @captureTelemetry(Telemetry.SelectRemoteJupyuterKernel) - public async selectRemoteJupyterKernel(connInfo: IConnection, currentKernel?: IJupyterKernelSpec | IJupyterKernel & Partial): Promise { + public async selectRemoteJupyterKernel(connInfo: IConnection, currentKernel?: IJupyterKernelSpec | LiveKernelModel): Promise { const session = await this.jupyterSessionManagerFactory.create(connInfo); return this.kernelSelector.selectRemoteKernel(session, undefined, currentKernel); } @@ -292,10 +308,7 @@ export class DataScience implements IDataScience { } } - private async startSelectingURI( - input: IMultiStepInput<{}>, - _state: {}): Promise | void> { - + private async startSelectingURI(input: IMultiStepInput<{}>, _state: {}): Promise | void> { // First step, show a quick pick to choose either the remote or the local. // newChoice element will be set if the user picked 'enter a new server' const item = await input.showQuickPick>({ @@ -312,9 +325,7 @@ export class DataScience implements IDataScience { } } - private async selectRemoteURI( - input: IMultiStepInput<{}>, - _state: {}): Promise | void> { + private async selectRemoteURI(input: IMultiStepInput<{}>, _state: {}): Promise | void> { // Ask the user to enter a URI to connect to. const uri = await input.showInputBox({ title: localize.DataScience.jupyterSelectURIPrompt(), @@ -354,12 +365,14 @@ export class DataScience implements IDataScience { const sessionManager = await waitForPromise(this.jupyterSessionManagerFactory.create(connection, true), 2000); if (sessionManager) { const kernels = await sessionManager.getRunningKernels(); - const lastActiveTime = kernels.map(k => k.lastActivityTime).reduce((p, c) => { - if (!p || c > p) { - return c; - } - return p; - }); + const lastActiveTime = kernels + .map(k => k.lastActivityTime) + .reduce((p, c) => { + if (!p || c > p) { + return c; + } + return p; + }); const activeConnectCount = kernels.map(k => k.numberOfConnections).reduce((p, c) => p + c); return { label: uri, @@ -439,8 +452,7 @@ export class DataScience implements IDataScience { if (activeEditor && activeCodeWatcher) { // Find the cell that matches return activeCodeWatcher.getCodeLenses().find((c: vscode.CodeLens) => { - if (c.range.end.line >= activeEditor.selection.anchor.line && - c.range.start.line <= activeEditor.selection.anchor.line) { + if (c.range.end.line >= activeEditor.selection.anchor.line && c.range.start.line <= activeEditor.selection.anchor.line) { return true; } return false; diff --git a/src/client/datascience/jupyter/jupyterNotebook.ts b/src/client/datascience/jupyter/jupyterNotebook.ts index c56a6b882733..e271c71de676 100644 --- a/src/client/datascience/jupyter/jupyterNotebook.ts +++ b/src/client/datascience/jupyter/jupyterNotebook.ts @@ -24,8 +24,20 @@ import { generateCells } from '../cellFactory'; import { CellMatcher } from '../cellMatcher'; import { concatMultilineStringInput, concatMultilineStringOutput, formatStreamText } from '../common'; import { CodeSnippits, Identifiers, Telemetry } from '../constants'; -import { CellState, ICell, IJupyterKernel, IJupyterKernelSpec, IJupyterSession, INotebook, INotebookCompletion, INotebookExecutionLogger, INotebookServer, INotebookServerLaunchInfo, InterruptResult } from '../types'; +import { + CellState, + ICell, + IJupyterKernelSpec, + IJupyterSession, + INotebook, + INotebookCompletion, + INotebookExecutionLogger, + INotebookServer, + INotebookServerLaunchInfo, + InterruptResult +} from '../types'; import { expandWorkingDir } from './jupyterUtils'; +import { LiveKernelModel } from './kernels/types'; // tslint:disable-next-line: no-require-imports import cloneDeep = require('lodash/cloneDeep'); @@ -115,8 +127,7 @@ class CellSubscriber { } private attemptToFinish() { - if ((!this.deferred.completed) && - (this.cell.state === CellState.finished || this.cell.state === CellState.error)) { + if (!this.deferred.completed && (this.cell.state === CellState.finished || this.cell.state === CellState.error)) { this.deferred.resolve(this.cell.state); this.promiseComplete(this); } @@ -221,10 +232,7 @@ export class JupyterNotebookBase implements INotebook { this.initializedMatplotlib = false; const configInit = !settings || settings.enablePlotViewer ? CodeSnippits.ConfigSvg : CodeSnippits.ConfigPng; traceInfo(`Initialize config for plots for ${this.resource.toString()}`); - await this.executeSilently( - configInit, - cancelToken - ); + await this.executeSilently(configInit, cancelToken); } // Run any startup commands that we specified. Support the old form too @@ -259,12 +267,13 @@ export class JupyterNotebookBase implements INotebook { (cells: ICell[]) => { output = cells; }, - (error) => { + error => { deferred.reject(error); }, () => { deferred.resolve(output); - }); + } + ); if (cancelToken) { this.disposableRegistry.push(cancelToken.onCancellationRequested(() => deferred.reject(new CancellationError()))); @@ -288,16 +297,18 @@ export class JupyterNotebookBase implements INotebook { const stopWatch = new StopWatch(); const result = this.executeObservableImpl(code, file, line, id, silent); return new Observable(subscriber => { - result.subscribe(cells => { - subscriber.next(cells); - }, + result.subscribe( + cells => { + subscriber.next(cells); + }, error => { subscriber.error(error); }, () => { subscriber.complete(); sendTelemetryEvent(Telemetry.ExecuteCell, stopWatch.elapsedTime); - }); + } + ); }); } @@ -318,11 +329,7 @@ export class JupyterNotebookBase implements INotebook { return { data: { cell_type: 'messages', - messages: [ - version, - notebookVersion, - pythonPath - ], + messages: [version, notebookVersion, pythonPath], metadata: {}, source: [] }, @@ -397,7 +404,8 @@ export class JupyterNotebookBase implements INotebook { const restartHandlerToken = this.session.onSessionStatusChanged(restartHandler); // Start our interrupt. If it fails, indicate a restart - this.session.interrupt(timeoutMs) + this.session + .interrupt(timeoutMs) .then(() => restarted.resolve([])) .catch(exc => { traceWarning(`Error during interrupt: ${exc}`); @@ -424,10 +432,9 @@ export class JupyterNotebookBase implements INotebook { // Indicate the interrupt worked. return InterruptResult.Success; - } catch (exc) { // Something failed. See if we restarted or not. - if (this.sessionStartTime && (interruptBeginTime < this.sessionStartTime)) { + if (this.sessionStartTime && interruptBeginTime < this.sessionStartTime) { return InterruptResult.Restarted; } @@ -450,18 +457,20 @@ export class JupyterNotebookBase implements INotebook { const settings = this.configService.getSettings().datascience; if (settings.themeMatplotlibPlots && !settings.ignoreVscodeTheme) { // Reset the matplotlib style based on if dark or not. - await this.executeSilently(useDark ? - 'matplotlib.style.use(\'dark_background\')' : - `matplotlib.rcParams.update(${Identifiers.MatplotLibDefaultParams})`); + await this.executeSilently(useDark ? 'matplotlib.style.use(\'dark_background\')' : `matplotlib.rcParams.update(${Identifiers.MatplotLibDefaultParams})`); } } public async getCompletion(cellCode: string, offsetInCode: number, cancelToken?: CancellationToken): Promise { if (this.session) { - const result = await Cancellation.race(() => this.session!.requestComplete({ - code: cellCode, - cursor_pos: offsetInCode - }), cancelToken); + const result = await Cancellation.race( + () => + this.session!.requestComplete({ + code: cellCode, + cursor_pos: offsetInCode + }), + cancelToken + ); if (result && result.content) { if ('matches' in result.content) { return { @@ -494,11 +503,11 @@ export class JupyterNotebookBase implements INotebook { this.launchInfo.interpreter = interpreter; } - public getKernelSpec(): IJupyterKernelSpec | IJupyterKernel & Partial | undefined { + public getKernelSpec(): IJupyterKernelSpec | LiveKernelModel | undefined { return this.launchInfo.kernelSpec; } - public async setKernelSpec(spec: IJupyterKernelSpec | IJupyterKernel & Partial): Promise { + public async setKernelSpec(spec: IJupyterKernelSpec | LiveKernelModel): Promise { // Change our own kernel spec this.launchInfo.kernelSpec = spec; @@ -523,10 +532,7 @@ export class JupyterNotebookBase implements INotebook { traceInfo(`Initialize matplotlib for ${this.resource.toString()}`); // Force matplotlib to inline and save the default style. We'll use this later if we // get a request to update style - await this.executeSilently( - matplobInit, - cancelToken - ); + await this.executeSilently(matplobInit, cancelToken); // Use this flag to detemine if we need to rerun this or not. this.initializedMatplotlib = true; @@ -552,12 +558,13 @@ export class JupyterNotebookBase implements INotebook { (cells: ICell[]) => { output = cells; }, - (error) => { + error => { deferred.reject(error); }, () => { deferred.resolve(output); - }); + } + ); if (cancelToken) { this.disposableRegistry.push(cancelToken.onCancellationRequested(() => deferred.reject(new CancellationError()))); @@ -598,13 +605,10 @@ export class JupyterNotebookBase implements INotebook { // Might have more than one (markdown might be split) if (cells.length > 1) { // We need to combine results - return this.combineObservables( - this.executeMarkdownObservable(cells[0]), - this.executeCodeObservable(cells[1], silent)); + return this.combineObservables(this.executeMarkdownObservable(cells[0]), this.executeCodeObservable(cells[1], silent)); } else if (cells.length > 0) { // Either markdown or or code - return this.combineObservables( - cells[0].data.cell_type === 'code' ? this.executeCodeObservable(cells[0], silent) : this.executeMarkdownObservable(cells[0])); + return this.combineObservables(cells[0].data.cell_type === 'code' ? this.executeCodeObservable(cells[0], silent) : this.executeMarkdownObservable(cells[0])); } } @@ -621,16 +625,18 @@ export class JupyterNotebookBase implements INotebook { //traceInfo(`Executing code in jupyter : ${code}`); try { const cellMatcher = new CellMatcher(this.configService.getSettings().datascience); - return this.session ? this.session.requestExecute( - { - // Remove the cell marker if we have one. - code: cellMatcher.stripFirstMarker(code), - stop_on_error: false, - allow_stdin: true, // Allow when silent too in case runStartupCommands asks for a password - store_history: !silent // Silent actually means don't output anything. Store_history is what affects execution_count - }, - true - ) : undefined; + return this.session + ? this.session.requestExecute( + { + // Remove the cell marker if we have one. + code: cellMatcher.stripFirstMarker(code), + stop_on_error: false, + allow_stdin: true, // Allow when silent too in case runStartupCommands asks for a password + store_history: !silent // Silent actually means don't output anything. Store_history is what affects execution_count + }, + true + ) + : undefined; } catch (exc) { // Any errors generating a request should just be logged. User can't do anything about it. traceError(exc); @@ -645,27 +651,29 @@ export class JupyterNotebookBase implements INotebook { const results: Record = {}; args.forEach(o => { - o.subscribe(c => { - results[c.id] = c; - - // Convert to an array - const array = Object.keys(results).map((k: string) => { - return results[k]; - }); - - // Update our subscriber of our total results if we have that many - if (array.length === args.length) { - subscriber.next(array); - - // Complete when everybody is finished - if (array.every(a => a.state === CellState.finished || a.state === CellState.error)) { - subscriber.complete(); + o.subscribe( + c => { + results[c.id] = c; + + // Convert to an array + const array = Object.keys(results).map((k: string) => { + return results[k]; + }); + + // Update our subscriber of our total results if we have that many + if (array.length === args.length) { + subscriber.next(array); + + // Complete when everybody is finished + if (array.every(a => a.state === CellState.finished || a.state === CellState.error)) { + subscriber.complete(); + } } - } - }, + }, e => { subscriber.error(e); - }); + } + ); }); }); } @@ -682,11 +690,11 @@ export class JupyterNotebookBase implements INotebook { if (this.launchInfo && this.launchInfo.connectionInfo.localLaunch && !this._workingDirectory) { // See what our working dir is supposed to be const suggested = this.launchInfo.workingDir; - if (suggested && await fs.pathExists(suggested)) { + if (suggested && (await fs.pathExists(suggested))) { // We should use the launch info directory. It trumps the possible dir this._workingDirectory = suggested; return this.changeDirectoryIfPossible(this._workingDirectory); - } else if (launchingFile && await fs.pathExists(launchingFile)) { + } else if (launchingFile && (await fs.pathExists(launchingFile))) { // Combine the working directory with this file if possible. this._workingDirectory = expandWorkingDir(this.launchInfo.workingDir, launchingFile, this.workspace); if (this._workingDirectory) { @@ -697,7 +705,7 @@ export class JupyterNotebookBase implements INotebook { } private changeDirectoryIfPossible = async (directory: string): Promise => { - if (this.launchInfo && this.launchInfo.connectionInfo.localLaunch && await fs.pathExists(directory)) { + if (this.launchInfo && this.launchInfo.connectionInfo.localLaunch && (await fs.pathExists(directory))) { await this.executeSilently(`%cd "${directory}"`); } } @@ -746,11 +754,10 @@ export class JupyterNotebookBase implements INotebook { private handleInputRequest(_subscriber: CellSubscriber, msg: KernelMessage.IStdinMessage) { // Ask the user for input if (msg.content && 'prompt' in msg.content && msg.content.prompt) { - const hasPassword = msg.content.password !== null && msg.content.password as boolean; - this.applicationService.showInputBox({ prompt: msg.content.prompt.toString(), password: hasPassword }) - .then(v => { - this.session.sendInputReply(v || ''); - }); + const hasPassword = msg.content.password !== null && (msg.content.password as boolean); + this.applicationService.showInputBox({ prompt: msg.content.prompt.toString(), password: hasPassword }).then(v => { + this.session.sendInputReply(v || ''); + }); } } @@ -758,7 +765,6 @@ export class JupyterNotebookBase implements INotebook { private handleCodeRequest = (subscriber: CellSubscriber, silent?: boolean) => { // Generate a new request if we still can if (subscriber.isValid(this.sessionStartTime)) { - // Double check process is still running if (this.launchInfo && this.launchInfo.connectionInfo && this.launchInfo.connectionInfo.localProcExitCode) { // Not running, just exit @@ -776,7 +782,7 @@ export class JupyterNotebookBase implements INotebook { let exitHandlerDisposable: Disposable | undefined; if (this.launchInfo && this.launchInfo.connectionInfo) { // If the server crashes, cancel the current observable - exitHandlerDisposable = this.launchInfo.connectionInfo.disconnected((c) => { + exitHandlerDisposable = this.launchInfo.connectionInfo.disconnected(c => { const str = c ? c.toString() : ''; // Only do an error if we're not disposed. If we're disposed we already shutdown. if (!this._disposed) { @@ -816,7 +822,8 @@ export class JupyterNotebookBase implements INotebook { if (exitHandlerDisposable) { exitHandlerDisposable.dispose(); } - }).ignoreErrors(); + }) + .ignoreErrors(); } else { subscriber.error(this.sessionStartTime, this.getDisposedError()); } @@ -831,7 +838,6 @@ export class JupyterNotebookBase implements INotebook { subscriber.cell.state = CellState.error; subscriber.complete(this.sessionStartTime); } - } private executeCodeObservable(cell: ICell, silent?: boolean): Observable { @@ -853,11 +859,12 @@ export class JupyterNotebookBase implements INotebook { this.pendingCellSubscriptions.push(cellSubscriber); // Log the pre execution. - this.logPreCode(cell, isSilent).then(() => { - // Now send our real request. This should call back on the cellsubscriber when it's done. - this.handleCodeRequest(cellSubscriber, silent); - }).ignoreErrors(); - + this.logPreCode(cell, isSilent) + .then(() => { + // Now send our real request. This should call back on the cellsubscriber when it's done. + this.handleCodeRequest(cellSubscriber, silent); + }) + .ignoreErrors(); }); } @@ -869,7 +876,11 @@ export class JupyterNotebookBase implements INotebook { await Promise.all(this._loggers.map(l => l.postExecute(cloneDeep(cell), silent))); } - private addToCellData = (cell: ICell, output: nbformat.IUnrecognizedOutput | nbformat.IExecuteResult | nbformat.IDisplayData | nbformat.IStream | nbformat.IError, clearState: Map) => { + private addToCellData = ( + cell: ICell, + output: nbformat.IUnrecognizedOutput | nbformat.IExecuteResult | nbformat.IDisplayData | nbformat.IStream | nbformat.IError, + clearState: Map + ) => { const data: nbformat.ICodeCell = cell.data as nbformat.ICodeCell; // If a clear is pending, replace the output with the new one @@ -895,7 +906,8 @@ export class JupyterNotebookBase implements INotebook { this.addToCellData( cell, { output_type: 'execute_result', data: msg.content.data, metadata: msg.content.metadata, execution_count: msg.content.execution_count }, - clearState); + clearState + ); } private handleExecuteInput(msg: KernelMessage.IExecuteInputMsg, _clearState: Map, cell: ICell) { @@ -924,7 +936,6 @@ export class JupyterNotebookBase implements INotebook { existing.text = existing.text + msg.content.text; existing.text = trimFunc(formatStreamText(concatMultilineStringOutput(existing.text))); } - } else { // Create a new stream entry const output: nbformat.IStream = { @@ -971,21 +982,22 @@ export class JupyterNotebookBase implements INotebook { } private handleInterrupted(cell: ICell) { - this.handleError({ - channel: 'iopub', - parent_header: {}, - metadata: {}, - header: { username: '', version: '', session: '', msg_id: '', msg_type: 'error', date: '' }, - content: { - ename: 'KeyboardInterrupt', - evalue: '', - // Does this need to be translated? All depends upon if jupyter does or not - traceback: [ - '---------------------------------------------------------------------------', - 'KeyboardInterrupt: ' - ] - } - }, new Map(), cell); + this.handleError( + { + channel: 'iopub', + parent_header: {}, + metadata: {}, + header: { username: '', version: '', session: '', msg_id: '', msg_type: 'error', date: '' }, + content: { + ename: 'KeyboardInterrupt', + evalue: '', + // Does this need to be translated? All depends upon if jupyter does or not + traceback: ['---------------------------------------------------------------------------', 'KeyboardInterrupt: '] + } + }, + new Map(), + cell + ); } private handleError(msg: KernelMessage.IErrorMsg, clearState: Map, cell: ICell) { diff --git a/src/client/datascience/jupyter/jupyterSession.ts b/src/client/datascience/jupyter/jupyterSession.ts index 809038d73b0c..e40ac7712325 100644 --- a/src/client/datascience/jupyter/jupyterSession.ts +++ b/src/client/datascience/jupyter/jupyterSession.ts @@ -14,23 +14,33 @@ import { traceInfo, traceWarning } from '../../common/logger'; import { sleep, waitForPromise } from '../../common/utils/async'; import * as localize from '../../common/utils/localize'; import { noop } from '../../common/utils/misc'; -import { IConnection, IJupyterKernel, IJupyterKernelSpec, IJupyterSession } from '../types'; +import { IConnection, IJupyterKernelSpec, IJupyterSession } from '../types'; import { JupyterWaitForIdleError } from './jupyterWaitForIdleError'; import { JupyterKernelPromiseFailedError } from './kernels/jupyterKernelPromiseFailedError'; import { KernelSelector } from './kernels/kernelSelector'; +import { LiveKernelModel } from './kernels/types'; + +type ISession = (Session.ISession & { + /** + * Whether this is a remote session that we attached to. + * + * @type {boolean} + */ + isRemoteSession?: boolean; +}); export class JupyterSession implements IJupyterSession { - private session: Session.ISession | undefined; - private restartSessionPromise: Promise | undefined; + private session: ISession | undefined; + private restartSessionPromise: Promise | undefined; private notebookFiles: Contents.IModel[] = []; private onStatusChangedEvent: EventEmitter = new EventEmitter(); - private statusHandler: Slot; + private statusHandler: Slot; private connected: boolean = false; constructor( private connInfo: IConnection, private serverSettings: ServerConnection.ISettings, - private kernelSpec: IJupyterKernelSpec | IJupyterKernel & Partial | undefined, + private kernelSpec: IJupyterKernelSpec | LiveKernelModel | undefined, private sessionManager: SessionManager, private contentsManager: ContentsManager, private readonly kernelSelector: KernelSelector @@ -64,7 +74,6 @@ export class JupyterSession implements IJupyterSession { traceInfo('Shutdown session - shutdown restart session'); await this.shutdownSession(restartSession, undefined); } - } catch { noop(); } @@ -130,11 +139,15 @@ export class JupyterSession implements IJupyterSession { } } - public requestExecute(content: KernelMessage.IExecuteRequestMsg['content'], disposeOnDone?: boolean, metadata?: JSONObject): Kernel.IShellFuture | undefined { + public requestExecute( + content: KernelMessage.IExecuteRequestMsg['content'], + disposeOnDone?: boolean, + metadata?: JSONObject + ): Kernel.IShellFuture | undefined { const result = this.session && this.session.kernel ? this.session.kernel.requestExecute(content, disposeOnDone, metadata) : undefined; // It has been observed that starting the restart session slows down first time to execute a cell. // Solution is to start the restart session after the first execution of user code. - if (!content.silent && result){ + if (!content.silent && result) { result.done.finally(() => this.startRestartSession()).ignoreErrors(); } return result; @@ -170,13 +183,18 @@ export class JupyterSession implements IJupyterSession { return this.connected; } - public async changeKernel(kernel: IJupyterKernelSpec | IJupyterKernel & Partial): Promise { - if (kernel.id && this.session){ + public async changeKernel(kernel: IJupyterKernelSpec | LiveKernelModel): Promise { + if (kernel.id && this.session && 'session' in kernel) { + // Shutdown the current session + this.shutdownSession(this.session, this.statusHandler).ignoreErrors(); + this.kernelSpec = kernel; - // tslint:disable-next-line: no-any - await this.session.changeKernel(kernel as any); + this.session = this.sessionManager.connectTo(kernel.session); + this.session.isRemoteSession = true; + this.session.statusChanged.connect(this.statusHandler); return; } + // This is just like doing a restart, kill the old session (and the old restart session), and start new ones if (this.session?.kernel) { this.shutdownSession(this.session, this.statusHandler).ignoreErrors(); @@ -226,20 +244,17 @@ export class JupyterSession implements IJupyterSession { } } - private async waitForIdleOnSession(session: Session.ISession | undefined, timeout: number): Promise { + private async waitForIdleOnSession(session: ISession | undefined, timeout: number): Promise { if (session && session.kernel) { traceInfo(`Waiting for idle on (kernel): ${session.kernel.id} -> ${session.kernel.status}`); - const statusChangedPromise = new Promise(resolve => session.kernelChanged.connect((_, e) => e.newValue && e.newValue.status === 'idle' ? resolve() : undefined)); + const statusChangedPromise = new Promise(resolve => session.kernelChanged.connect((_, e) => (e.newValue && e.newValue.status === 'idle' ? resolve() : undefined))); const checkStatusPromise = new Promise(async resolve => { // This function seems to cause CI builds to timeout randomly on // different tests. Waiting for status to go idle doesn't seem to work and // in the past, waiting on the ready promise doesn't work either. Check status with a maximum of 5 seconds const startTime = Date.now(); - while (session && - session.kernel && - session.kernel.status !== 'idle' && - (Date.now() - startTime < timeout)) { + while (session && session.kernel && session.kernel.status !== 'idle' && Date.now() - startTime < timeout) { await sleep(100); } resolve(); @@ -256,8 +271,8 @@ export class JupyterSession implements IJupyterSession { } } - private async createRestartSession(serverSettings: ServerConnection.ISettings, contentsManager: ContentsManager, cancelToken?: CancellationToken): Promise { - let result: Session.ISession | undefined; + private async createRestartSession(serverSettings: ServerConnection.ISettings, contentsManager: ContentsManager, cancelToken?: CancellationToken): Promise { + let result: ISession | undefined; let tryCount = 0; // tslint:disable-next-line: no-any let exception: any; @@ -281,7 +296,6 @@ export class JupyterSession implements IJupyterSession { } private async createSession(serverSettings: ServerConnection.ISettings, contentsManager: ContentsManager, cancelToken?: CancellationToken): Promise { - // Create a temporary notebook for this session. this.notebookFiles.push(await contentsManager.newUntitled({ type: 'notebook' })); @@ -315,7 +329,7 @@ export class JupyterSession implements IJupyterSession { } } - private async shutdownSession(session: Session.ISession | undefined, statusHandler: Slot | undefined): Promise { + private async shutdownSession(session: ISession | undefined, statusHandler: Slot | undefined): Promise { if (session && session.kernel) { const kernelId = session.kernel.id; traceInfo(`shutdownSession ${kernelId} - start`); @@ -323,6 +337,10 @@ export class JupyterSession implements IJupyterSession { if (statusHandler) { session.statusChanged.disconnect(statusHandler); } + // Do not shutdown remote sessions. + if (session.isRemoteSession){ + return; + } try { // When running under a test, mark all futures as done so we // don't hit this problem: diff --git a/src/client/datascience/jupyter/jupyterSessionManager.ts b/src/client/datascience/jupyter/jupyterSessionManager.ts index ecf2be3a7199..0fac01b6a0aa 100644 --- a/src/client/datascience/jupyter/jupyterSessionManager.ts +++ b/src/client/datascience/jupyter/jupyterSessionManager.ts @@ -1,29 +1,21 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. 'use strict'; -import { ContentsManager, Kernel, ServerConnection, SessionManager } from '@jupyterlab/services'; +import { ContentsManager, Kernel, ServerConnection, Session, SessionManager } from '@jupyterlab/services'; import { Agent as HttpsAgent } from 'https'; import { CancellationToken } from 'vscode-jsonrpc'; import { traceInfo } from '../../common/logger'; import { IConfigurationService } from '../../common/types'; import * as localize from '../../common/utils/localize'; -import { - IConnection, - IJupyterKernel, - IJupyterKernelSpec, - IJupyterPasswordConnect, - IJupyterPasswordConnectInfo, - IJupyterSession, - IJupyterSessionManager -} from '../types'; +import { IConnection, IJupyterKernel, IJupyterKernelSpec, IJupyterPasswordConnect, IJupyterPasswordConnectInfo, IJupyterSession, IJupyterSessionManager } from '../types'; import { JupyterSession } from './jupyterSession'; import { createJupyterWebSocket } from './jupyterWebSocket'; import { JupyterKernelSpec } from './kernels/jupyterKernelSpec'; import { KernelSelector } from './kernels/kernelSelector'; +import { LiveKernelModel } from './kernels/types'; export class JupyterSessionManager implements IJupyterSessionManager { - private sessionManager: SessionManager | undefined; private contentsManager: ContentsManager | undefined; private connInfo: IConnection | undefined; @@ -61,11 +53,31 @@ export class JupyterSessionManager implements IJupyterSessionManager { this.contentsManager = new ContentsManager({ serverSettings: this.serverSettings }); } + public async getRunningSessions(): Promise { + if (!this.sessionManager) { + return []; + } + // Not refreshing will result in `running` returning an empty iterator. + await this.sessionManager.refreshRunning(); + + const sessions: Session.IModel[] = []; + const iterator = this.sessionManager.running(); + let session = iterator.next(); + + while (session) { + sessions.push(session); + session = iterator.next(); + } + + return sessions; + } + public async getRunningKernels(): Promise { const models = await Kernel.listRunning(this.serverSettings); // Remove duplicates. const dup = new Set(); - return models.map(m => { + return models + .map(m => { return { id: m.id, name: m.name, @@ -74,7 +86,7 @@ export class JupyterSessionManager implements IJupyterSessionManager { }; }) .filter(item => { - if (dup.has(item.id)){ + if (dup.has(item.id)) { return false; } dup.add(item.id); @@ -82,7 +94,7 @@ export class JupyterSessionManager implements IJupyterSessionManager { }); } - public async startNew(kernelSpec: IJupyterKernelSpec | IJupyterKernel & Partial | undefined, cancelToken?: CancellationToken): Promise { + public async startNew(kernelSpec: IJupyterKernelSpec | LiveKernelModel | undefined, cancelToken?: CancellationToken): Promise { if (!this.connInfo || !this.sessionManager || !this.contentsManager || !this.serverSettings) { throw new Error(localize.DataScience.sessionDisposed()); } @@ -124,8 +136,7 @@ export class JupyterSessionManager implements IJupyterSessionManager { } private async getServerConnectSettings(connInfo: IConnection): Promise { - let serverSettings: Partial = - { + let serverSettings: Partial = { baseUrl: connInfo.baseUrl, appUrl: '', // A web socket is required to allow token authentication @@ -170,8 +181,12 @@ export class JupyterSessionManager implements IJupyterSessionManager { // This replaces the WebSocket constructor in jupyter lab services with our own implementation // See _createSocket here: // https://github.com/jupyterlab/jupyterlab/blob/cfc8ebda95e882b4ed2eefd54863bb8cdb0ab763/packages/services/src/kernel/default.ts - // tslint:disable-next-line:no-any - serverSettings = { ...serverSettings, init: requestInit, WebSocket: createJupyterWebSocket(this.config.getSettings().datascience.verboseLogging, cookieString, allowUnauthorized) as any }; + serverSettings = { + ...serverSettings, + init: requestInit, + // tslint:disable-next-line:no-any + WebSocket: createJupyterWebSocket(this.config.getSettings().datascience.verboseLogging, cookieString, allowUnauthorized) as any + }; return ServerConnection.makeSettings(serverSettings); } diff --git a/src/client/datascience/jupyter/kernels/kernelSelections.ts b/src/client/datascience/jupyter/kernels/kernelSelections.ts index 332b4fbc1733..fefc60ee6612 100644 --- a/src/client/datascience/jupyter/kernels/kernelSelections.ts +++ b/src/client/datascience/jupyter/kernels/kernelSelections.ts @@ -10,9 +10,9 @@ import { IFileSystem } from '../../../common/platform/types'; import { IPathUtils } from '../../../common/types'; import * as localize from '../../../common/utils/localize'; import { IInterpreterSelector } from '../../../interpreter/configuration/types'; -import { IJupyterKernel, IJupyterKernelSpec, IJupyterSessionManager } from '../../types'; +import { IJupyterKernelSpec, IJupyterSessionManager } from '../../types'; import { KernelService } from './kernelService'; -import { IKernelSelectionListProvider, IKernelSpecQuickPickItem } from './types'; +import { IKernelSelectionListProvider, IKernelSpecQuickPickItem, LiveKernelModel } from './types'; // Small classes, hence all put into one file. // tslint:disable: max-classes-per-file @@ -35,10 +35,10 @@ function getQuickPickItemForKernelSpec(kernelSpec: IJupyterKernelSpec, pathUtils /** * Given an active kernel, this will return a quick pick item with appropriate display names and the like. * - * @param {(IJupyterKernel & Partial)} kernel + * @param {(LiveKernelModel)} kernel * @returns {IKernelSpecQuickPickItem} */ -function getQuickPickItemForActiveKernel(kernel: IJupyterKernel & Partial, pathUtils: IPathUtils): IKernelSpecQuickPickItem { +function getQuickPickItemForActiveKernel(kernel: LiveKernelModel, pathUtils: IPathUtils): IKernelSpecQuickPickItem { const path = kernel.metadata?.interpreter?.path || kernel.path; return { label: kernel.display_name || kernel.name || '', @@ -59,16 +59,21 @@ function getQuickPickItemForActiveKernel(kernel: IJupyterKernel & Partial { - const [activeKernels, kernelSpecs] = await Promise.all([this.sessionManager.getRunningKernels(), this.sessionManager.getKernelSpecs()]); - const items = activeKernels.map(item => { - const matchingSpec: Partial = kernelSpecs.find(spec => spec.name === item.name) || {}; + const [activeKernels, activeSessions, kernelSpecs] = await Promise.all([this.sessionManager.getRunningKernels(), this.sessionManager.getRunningSessions(), this.sessionManager.getKernelSpecs()]); + const items = activeSessions.map(item => { + const matchingSpec: Partial = kernelSpecs.find(spec => spec.name === item.kernel.name) || {}; + const activeKernel = activeKernels.find(active => active.id === item.kernel.id) || {}; + // tslint:disable-next-line: no-object-literal-type-assertion return { - ...item, - ...matchingSpec - }; + ...item.kernel, + ...matchingSpec, + ...activeKernel, + session: item + } as LiveKernelModel; }); return items .filter(item => item.display_name || item.name) + .filter(item => 'lastActivityTime' in item && 'numberOfConnections' in item) .filter(item => (item.language || '').toLowerCase() === PYTHON_LANGUAGE.toLowerCase()) .map(item => getQuickPickItemForActiveKernel(item, this.pathUtils)); } @@ -86,9 +91,9 @@ export class InstalledJupyterKernelSelectionListProvider implements IKernelSelec public async getKernelSelections(cancelToken?: CancellationToken | undefined): Promise { const items = await this.kernelService.getKernelSpecs(this.sessionManager, cancelToken); return items - .filter(item => (item.language || '').toLowerCase() === PYTHON_LANGUAGE.toLowerCase()) + .filter(item => (item.language || '').toLowerCase() === PYTHON_LANGUAGE.toLowerCase()) .map(item => getQuickPickItemForKernelSpec(item, this.pathUtils)); - } + } } /** @@ -128,7 +133,8 @@ export class KernelSelectionProvider { @inject(KernelService) private readonly kernelService: KernelService, @inject(IInterpreterSelector) private readonly interpreterSelector: IInterpreterSelector, @inject(IFileSystem) private readonly fileSystem: IFileSystem, - @inject(IPathUtils) private readonly pathUtils: IPathUtils) {} + @inject(IPathUtils) private readonly pathUtils: IPathUtils + ) {} /** * Gets a selection of kernel specs from a remote session. * @@ -170,27 +176,34 @@ export class KernelSelectionProvider { // tslint:disable-next-line: prefer-const let [installedKernels, interpreters] = await Promise.all([installedKernelsPromise, interpretersPromise]); - interpreters = interpreters.filter(item => { - // If the interpreter is registered as a kernel then don't inlcude it. - if (installedKernels.find(installedKernel => installedKernel.selection.kernelSpec?.display_name === item.selection.interpreter?.displayName && ( - this.fileSystem.arePathsSame((installedKernel.selection.kernelSpec?.argv || [])[0], item.selection.interpreter?.path || '') || - this.fileSystem.arePathsSame(installedKernel.selection.kernelSpec?.metadata?.interpreter?.path || '', item.selection.interpreter?.path || '')))) { - return false; - } - return true; - }).map(item => { - // We don't want descriptions. - return {...item, description: ''}; - }); + interpreters = interpreters + .filter(item => { + // If the interpreter is registered as a kernel then don't inlcude it. + if ( + installedKernels.find( + installedKernel => + installedKernel.selection.kernelSpec?.display_name === item.selection.interpreter?.displayName && + (this.fileSystem.arePathsSame((installedKernel.selection.kernelSpec?.argv || [])[0], item.selection.interpreter?.path || '') || + this.fileSystem.arePathsSame(installedKernel.selection.kernelSpec?.metadata?.interpreter?.path || '', item.selection.interpreter?.path || '')) + ) + ) { + return false; + } + return true; + }) + .map(item => { + // We don't want descriptions. + return { ...item, description: '' }; + }); const unifiedList = [...installedKernels!, ...interpreters]; // Sorty by name. - unifiedList.sort((a, b) => a.label === b.label ? 0 : (a.label > b.label ? 1 : -1)); + unifiedList.sort((a, b) => (a.label === b.label ? 0 : a.label > b.label ? 1 : -1)); return unifiedList; }; - const liveItems = getSelections().then(items => this.localSuggestionsCache = items); + const liveItems = getSelections().then(items => (this.localSuggestionsCache = items)); // If we have someting in cache, return that, while fetching in the background. const cachedItems = this.localSuggestionsCache.length > 0 ? Promise.resolve(this.localSuggestionsCache) : liveItems; return Promise.race([cachedItems, liveItems]); diff --git a/src/client/datascience/jupyter/kernels/kernelSelector.ts b/src/client/datascience/jupyter/kernels/kernelSelector.ts index 0e4ddd0d00bb..61d926f1fdfa 100644 --- a/src/client/datascience/jupyter/kernels/kernelSelector.ts +++ b/src/client/datascience/jupyter/kernels/kernelSelector.ts @@ -15,10 +15,10 @@ import { noop } from '../../../common/utils/misc'; import { IInterpreterService, PythonInterpreter } from '../../../interpreter/contracts'; import { sendTelemetryEvent } from '../../../telemetry'; import { Telemetry } from '../../constants'; -import { IJupyterKernel, IJupyterKernelSpec, IJupyterSessionManager } from '../../types'; +import { IJupyterKernelSpec, IJupyterSessionManager } from '../../types'; import { KernelSelectionProvider } from './kernelSelections'; import { KernelService } from './kernelService'; -import { IKernelSpecQuickPickItem } from './types'; +import { IKernelSpecQuickPickItem, LiveKernelModel } from './types'; export type KernelSpecInterpreter = { kernelSpec?: IJupyterKernelSpec; @@ -34,9 +34,9 @@ export type KernelSpecInterpreter = { * Active kernel from an active session. * If this is available, then user needs to connect to an existing kernel (instead of starting a new session). * - * @type {(IJupyterKernel & Partial)} + * @type {(LiveKernelModel)} */ - kernelModel?: IJupyterKernel & Partial; + kernelModel?: LiveKernelModel; }; @injectable() @@ -86,7 +86,7 @@ export class KernelSelector { * @returns {Promise} * @memberof KernelSelector */ - public async selectRemoteKernel(session: IJupyterSessionManager, cancelToken?: CancellationToken, currentKernel?: IJupyterKernelSpec | IJupyterKernel & Partial): Promise { + public async selectRemoteKernel(session: IJupyterSessionManager, cancelToken?: CancellationToken, currentKernel?: IJupyterKernelSpec | LiveKernelModel): Promise { let suggestions = await this.selectionProvider.getKernelSelectionsForRemoteSession(session, cancelToken); suggestions = suggestions.filter(item => !this.kernelIdsToHide.has(item.selection.kernelModel?.id || '')); return this.selectKernel(suggestions, session, cancelToken, currentKernel); @@ -99,7 +99,7 @@ export class KernelSelector { * @returns {Promise} * @memberof KernelSelector */ - public async selectLocalKernel(session?: IJupyterSessionManager, cancelToken?: CancellationToken, currentKernel?: IJupyterKernelSpec | IJupyterKernel & Partial): Promise { + public async selectLocalKernel(session?: IJupyterSessionManager, cancelToken?: CancellationToken, currentKernel?: IJupyterKernelSpec | LiveKernelModel): Promise { let suggestions = await this.selectionProvider.getKernelSelectionsForLocalSession(session, cancelToken); suggestions = suggestions.filter(item => !this.kernelIdsToHide.has(item.selection.kernelModel?.id || '')); return this.selectKernel(suggestions, session, cancelToken, currentKernel); @@ -217,7 +217,12 @@ export class KernelSelector { interpreter: interpreter }; } - private async selectKernel(suggestions: IKernelSpecQuickPickItem[], session?: IJupyterSessionManager, cancelToken?: CancellationToken, currentKernel?: IJupyterKernelSpec | IJupyterKernel & Partial) { + private async selectKernel( + suggestions: IKernelSpecQuickPickItem[], + session?: IJupyterSessionManager, + cancelToken?: CancellationToken, + currentKernel?: IJupyterKernelSpec | LiveKernelModel + ) { const placeHolder = localize.DataScience.selectKernel() + (currentKernel ? ` (current: ${currentKernel.display_name || currentKernel.name})` : ''); const selection = await this.applicationShell.showQuickPick(suggestions, { placeHolder }, cancelToken); if (!selection?.selection) { diff --git a/src/client/datascience/jupyter/kernels/kernelService.ts b/src/client/datascience/jupyter/kernels/kernelService.ts index 000f5bb4acd0..f42d93a90faa 100644 --- a/src/client/datascience/jupyter/kernels/kernelService.ts +++ b/src/client/datascience/jupyter/kernels/kernelService.ts @@ -22,9 +22,10 @@ import { IEnvironmentActivationService } from '../../../interpreter/activation/t import { IInterpreterService, PythonInterpreter } from '../../../interpreter/contracts'; import { captureTelemetry, sendTelemetryEvent } from '../../../telemetry'; import { JupyterCommands, Telemetry } from '../../constants'; -import { IJupyterKernel, IJupyterKernelSpec, IJupyterSessionManager } from '../../types'; +import { IJupyterKernelSpec, IJupyterSessionManager } from '../../types'; import { JupyterCommandFinder } from '../jupyterCommandFinder'; import { JupyterKernelSpec } from './jupyterKernelSpec'; +import { LiveKernelModel } from './types'; // tslint:disable-next-line: no-var-requires no-require-imports const NamedRegexp = require('named-js-regexp'); @@ -116,7 +117,7 @@ export class KernelService { * @returns {(Promise)} * @memberof KernelService */ - public async findMatchingInterpreter(kernelSpec: IJupyterKernelSpec | IJupyterKernel & Partial, cancelToken?: CancellationToken): Promise { + public async findMatchingInterpreter(kernelSpec: IJupyterKernelSpec | LiveKernelModel, cancelToken?: CancellationToken): Promise { if (kernelSpec.language && kernelSpec.language?.toLowerCase() !== PYTHON_LANGUAGE) { return; } @@ -255,11 +256,11 @@ export class KernelService { } let kernel = await this.findMatchingKernelSpec({ display_name: interpreter.displayName, name }, undefined, cancelToken); - for (let counter = 0; counter < 5; counter += 1){ + for (let counter = 0; counter < 5; counter += 1) { if (Cancellation.isCanceled(cancelToken)) { return; } - if (kernel){ + if (kernel) { break; } traceWarning('Waiting for 500ms for registered kernel to get detected'); diff --git a/src/client/datascience/jupyter/kernels/types.ts b/src/client/datascience/jupyter/kernels/types.ts index 2328bc646d51..bc541b01b853 100644 --- a/src/client/datascience/jupyter/kernels/types.ts +++ b/src/client/datascience/jupyter/kernels/types.ts @@ -3,10 +3,13 @@ 'use strict'; +import { Session } from '@jupyterlab/services'; import { CancellationToken, QuickPickItem } from 'vscode'; import { PythonInterpreter } from '../../../interpreter/contracts'; import { IJupyterKernel, IJupyterKernelSpec } from '../../types'; +export type LiveKernelModel = IJupyterKernel & Partial & { session: Session.IModel }; + export interface IKernelSpecQuickPickItem extends QuickPickItem { /** * Whether a @@ -21,7 +24,7 @@ export interface IKernelSpecQuickPickItem extends QuickPickItem { * @memberof IKernelSpecQuickPickItem */ selection: - | { kernelModel: IJupyterKernel & Partial; kernelSpec: undefined; interpreter: undefined } + | { kernelModel: LiveKernelModel; kernelSpec: undefined; interpreter: undefined } | { kernelModel: undefined; kernelSpec: IJupyterKernelSpec; interpreter: undefined } | { kernelModel: undefined; kernelSpec: undefined; interpreter: PythonInterpreter }; } diff --git a/src/client/datascience/jupyter/liveshare/guestJupyterNotebook.ts b/src/client/datascience/jupyter/liveshare/guestJupyterNotebook.ts index d41ecd174f40..6772073f837e 100644 --- a/src/client/datascience/jupyter/liveshare/guestJupyterNotebook.ts +++ b/src/client/datascience/jupyter/liveshare/guestJupyterNotebook.ts @@ -15,13 +15,13 @@ import * as localize from '../../../common/utils/localize'; import { noop } from '../../../common/utils/misc'; import { PythonInterpreter } from '../../../interpreter/contracts'; import { LiveShare, LiveShareCommands } from '../../constants'; -import { ICell, IJupyterKernel, IJupyterKernelSpec, INotebook, INotebookCompletion, INotebookExecutionLogger, INotebookServer, InterruptResult } from '../../types'; +import { ICell, IJupyterKernelSpec, INotebook, INotebookCompletion, INotebookExecutionLogger, INotebookServer, InterruptResult } from '../../types'; +import { LiveKernelModel } from '../kernels/types'; import { LiveShareParticipantDefault, LiveShareParticipantGuest } from './liveShareParticipantMixin'; import { ResponseQueue } from './responseQueue'; import { IExecuteObservableResponse, ILiveShareParticipant, IServerResponse } from './types'; -export class GuestJupyterNotebook - extends LiveShareParticipantGuest(LiveShareParticipantDefault, LiveShare.JupyterNotebookSharedService) +export class GuestJupyterNotebook extends LiveShareParticipantGuest(LiveShareParticipantDefault, LiveShare.JupyterNotebookSharedService) implements INotebook, ILiveShareParticipant { private responseQueue: ResponseQueue = new ResponseQueue(); private onStatusChangedEvent: EventEmitter | undefined; @@ -33,7 +33,6 @@ export class GuestJupyterNotebook private _resource: Uri, private _owner: INotebookServer, private startTime: number - ) { super(liveShare); } @@ -85,12 +84,13 @@ export class GuestJupyterNotebook (cells: ICell[]) => { output = cells; }, - (error) => { + error => { deferred.reject(error); }, () => { deferred.resolve(output); - }); + } + ); if (cancelToken) { this.disposableRegistry.push(cancelToken.onCancellationRequested(() => deferred.reject(new CancellationError()))); @@ -115,11 +115,13 @@ export class GuestJupyterNotebook public executeObservable(code: string, file: string, line: number, id: string): Observable { // Mimic this to the other side and then wait for a response - this.waitForService().then(s => { - if (s) { - s.notify(LiveShareCommands.executeObservable, { code, file, line, id }); - } - }).ignoreErrors(); + this.waitForService() + .then(s => { + if (s) { + s.notify(LiveShareCommands.executeObservable, { code, file, line, id }); + } + }) + .ignoreErrors(); return this.responseQueue.waitForObservable(code, id); } @@ -133,7 +135,7 @@ export class GuestJupyterNotebook const interruptTimeout = settings.datascience.jupyterInterruptTimeout; const response = await this.sendRequest(LiveShareCommands.interrupt, [interruptTimeout]); - return (response as InterruptResult); + return response as InterruptResult; } public async waitForServiceName(): Promise { @@ -148,7 +150,7 @@ export class GuestJupyterNotebook const service = await this.waitForService(); if (service) { const result = await service.request(LiveShareCommands.getSysInfo, []); - return (result as ICell); + return result as ICell; } } @@ -193,11 +195,11 @@ export class GuestJupyterNotebook noop(); } - public getKernelSpec(): IJupyterKernelSpec | IJupyterKernel & Partial | undefined { + public getKernelSpec(): IJupyterKernelSpec | LiveKernelModel | undefined { return; } - public setKernelSpec(_spec: IJupyterKernelSpec | IJupyterKernel & Partial): Promise { + public setKernelSpec(_spec: IJupyterKernelSpec | LiveKernelModel): Promise { return Promise.resolve(); } @@ -217,5 +219,4 @@ export class GuestJupyterNotebook return service.request(command, args); } } - } diff --git a/src/client/datascience/jupyter/liveshare/guestJupyterSessionManager.ts b/src/client/datascience/jupyter/liveshare/guestJupyterSessionManager.ts index 8e1d44ce3eae..41f94e5e1170 100644 --- a/src/client/datascience/jupyter/liveshare/guestJupyterSessionManager.ts +++ b/src/client/datascience/jupyter/liveshare/guestJupyterSessionManager.ts @@ -3,8 +3,10 @@ 'use strict'; import { CancellationToken } from 'vscode-jsonrpc'; +import { Session } from '@jupyterlab/services'; import { noop } from '../../../common/utils/misc'; import { IConnection, IJupyterKernel, IJupyterKernelSpec, IJupyterSession, IJupyterSessionManager } from '../../types'; +import { LiveKernelModel } from '../kernels/types'; export class GuestJupyterSessionManager implements IJupyterSessionManager { private connInfo: IConnection | undefined; @@ -13,7 +15,7 @@ export class GuestJupyterSessionManager implements IJupyterSessionManager { noop(); } - public startNew(kernelSpec: IJupyterKernelSpec | IJupyterKernel & Partial | undefined, cancelToken?: CancellationToken): Promise { + public startNew(kernelSpec: IJupyterKernelSpec | LiveKernelModel | undefined, cancelToken?: CancellationToken): Promise { return this.realSessionManager.startNew(kernelSpec, cancelToken); } @@ -26,6 +28,10 @@ export class GuestJupyterSessionManager implements IJupyterSessionManager { return Promise.resolve([]); } + public getRunningSessions(): Promise { + return Promise.resolve([]); + } + public async dispose(): Promise { noop(); } @@ -37,5 +43,4 @@ export class GuestJupyterSessionManager implements IJupyterSessionManager { public getConnInfo(): IConnection { return this.connInfo!; } - } diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index 7a555007b822..d3f77d6dd7d5 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. 'use strict'; import { nbformat } from '@jupyterlab/coreutils'; +import { Session } from '@jupyterlab/services'; import { Kernel, KernelMessage } from '@jupyterlab/services/lib/kernel'; import { JSONObject } from '@phosphor/coreutils'; import { Observable } from 'rxjs/Observable'; @@ -14,6 +15,7 @@ import { StopWatch } from '../common/utils/stopWatch'; import { PythonInterpreter } from '../interpreter/contracts'; import { JupyterCommands } from './constants'; import { KernelSpecInterpreter } from './jupyter/kernels/kernelSelector'; +import { LiveKernelModel } from './jupyter/kernels/types'; // Main interface export const IDataScience = Symbol('IDataScience'); @@ -58,7 +60,7 @@ export interface INotebookServerLaunchInfo { */ interpreter: PythonInterpreter | undefined; uri: string | undefined; // Different from the connectionInfo as this is the setting used, not the result - kernelSpec: IJupyterKernelSpec | undefined | IJupyterKernel & Partial; + kernelSpec: IJupyterKernelSpec | undefined | LiveKernelModel; workingDir: string | undefined; purpose: string | undefined; // Purpose this server is for enableDebugging: boolean | undefined; // If we should enable debugging for this server @@ -101,8 +103,8 @@ export interface INotebook extends IAsyncDisposable { setMatplotLibStyle(useDark: boolean): Promise; addLogger(logger: INotebookExecutionLogger): void; getMatchingInterpreter(): PythonInterpreter | undefined; - getKernelSpec(): IJupyterKernelSpec | IJupyterKernel & Partial | undefined; - setKernelSpec(spec: IJupyterKernelSpec | IJupyterKernel & Partial): Promise; + getKernelSpec(): IJupyterKernelSpec | LiveKernelModel | undefined; + setKernelSpec(spec: IJupyterKernelSpec | LiveKernelModel): Promise; setInterpreter(interpeter: PythonInterpreter): void; } @@ -170,10 +172,14 @@ export interface IJupyterSession extends IAsyncDisposable { restart(timeout: number): Promise; interrupt(timeout: number): Promise; waitForIdle(timeout: number): Promise; - requestExecute(content: KernelMessage.IExecuteRequestMsg['content'], disposeOnDone?: boolean, metadata?: JSONObject): Kernel.IShellFuture | undefined; + requestExecute( + content: KernelMessage.IExecuteRequestMsg['content'], + disposeOnDone?: boolean, + metadata?: JSONObject + ): Kernel.IShellFuture | undefined; requestComplete(content: KernelMessage.ICompleteRequestMsg['content']): Promise; sendInputReply(content: string): void; - changeKernel(kernel: IJupyterKernelSpec | IJupyterKernel & Partial): Promise; + changeKernel(kernel: IJupyterKernelSpec | LiveKernelModel): Promise; } export const IJupyterSessionManagerFactory = Symbol('IJupyterSessionManagerFactory'); @@ -182,10 +188,11 @@ export interface IJupyterSessionManagerFactory { } export interface IJupyterSessionManager extends IAsyncDisposable { - startNew(kernelSpec: IJupyterKernelSpec | IJupyterKernel & Partial | undefined, cancelToken?: CancellationToken): Promise; + startNew(kernelSpec: IJupyterKernelSpec | LiveKernelModel | undefined, cancelToken?: CancellationToken): Promise; getKernelSpecs(): Promise; getConnInfo(): IConnection; getRunningKernels(): Promise; + getRunningSessions(): Promise; } export interface IJupyterKernel { @@ -558,7 +565,7 @@ export interface ICellHash { runtimeLine: number; // Line in the jupyter source to start at hash: string; executionCount: number; - id: string; // Cell id as sent to jupyter + id: string; // Cell id as sent to jupyter } export interface IFileHashes { @@ -587,5 +594,4 @@ export const IDebugLocationTracker = Symbol('IDebugLocationTracker'); export interface IDebugLocationTracker { updated: Event; getLocation(debugSession: DebugSession): IDebugLocation | undefined; - } diff --git a/src/test/datascience/datascience.unit.test.ts b/src/test/datascience/datascience.unit.test.ts index 70c47bff521a..35ecb5255c50 100644 --- a/src/test/datascience/datascience.unit.test.ts +++ b/src/test/datascience/datascience.unit.test.ts @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. + // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. 'use strict'; import { assert } from 'chai'; diff --git a/src/test/datascience/execution.unit.test.ts b/src/test/datascience/execution.unit.test.ts index 8a921ebf07bd..e2c06cbf4583 100644 --- a/src/test/datascience/execution.unit.test.ts +++ b/src/test/datascience/execution.unit.test.ts @@ -46,11 +46,11 @@ import { JupyterCommandFactory } from '../../client/datascience/jupyter/jupyterC import { JupyterCommandFinder } from '../../client/datascience/jupyter/jupyterCommandFinder'; import { JupyterExecutionFactory } from '../../client/datascience/jupyter/jupyterExecutionFactory'; import { KernelSelector } from '../../client/datascience/jupyter/kernels/kernelSelector'; +import { LiveKernelModel } from '../../client/datascience/jupyter/kernels/types'; import { NotebookStarter } from '../../client/datascience/jupyter/notebookStarter'; import { ICell, IConnection, - IJupyterKernel, IJupyterKernelSpec, INotebook, INotebookCompletion, @@ -146,7 +146,7 @@ class MockJupyterNotebook implements INotebook { return; } - public setKernelSpec(_spec: IJupyterKernelSpec | IJupyterKernel & Partial): Promise { + public setKernelSpec(_spec: IJupyterKernelSpec | LiveKernelModel): Promise { return Promise.resolve(); } @@ -160,7 +160,6 @@ class MockJupyterNotebook implements INotebook { // tslint:disable:no-any no-http-string no-multiline-string max-func-body-length class MockJupyterServer implements INotebookServer { - private launchInfo: INotebookServerLaunchInfo | undefined; private notebookFile: TemporaryFile | undefined; private _id = uuid(); @@ -233,7 +232,6 @@ class DisposableRegistry implements IDisposableRegistry, IAsyncDisposableRegistr } this.disposables = []; } - } suite('Jupyter Execution', async () => { @@ -338,18 +336,11 @@ suite('Jupyter Execution', async () => { const subDir = uuid(); const filePath = path.join(tempDir, subDir, 'kernel.json'); fs.ensureDirSync(path.dirname(filePath)); - fs.writeJSONSync(filePath, - { - display_name: 'Python 3', - language: 'python', - argv: [ - pythonPath, - '-m', - 'ipykernel_launcher', - '-f', - '{connection_file}' - ] - }); + fs.writeJSONSync(filePath, { + display_name: 'Python 3', + language: 'python', + argv: [pythonPath, '-m', 'ipykernel_launcher', '-f', '{connection_file}'] + }); return filePath; } @@ -377,42 +368,69 @@ suite('Jupyter Execution', async () => { } function setupPythonService(service: TypeMoq.IMock, module: string | undefined, args: (string | RegExp)[], result: Promise>) { - if (module) { - service.setup(x => x.execModule( - TypeMoq.It.isValue(module), - TypeMoq.It.is(a => argsMatch(args, a)), - TypeMoq.It.isAny())) + service + .setup(x => + x.execModule( + TypeMoq.It.isValue(module), + TypeMoq.It.is(a => argsMatch(args, a)), + TypeMoq.It.isAny() + ) + ) .returns(() => result); const withModuleArgs = ['-m', module, ...args]; - service.setup(x => x.exec( - TypeMoq.It.is(a => argsMatch(withModuleArgs, a)), - TypeMoq.It.isAny())) + service + .setup(x => + x.exec( + TypeMoq.It.is(a => argsMatch(withModuleArgs, a)), + TypeMoq.It.isAny() + ) + ) .returns(() => result); } else { - service.setup(x => x.exec( - TypeMoq.It.is(a => argsMatch(args, a)), - TypeMoq.It.isAny())) + service + .setup(x => + x.exec( + TypeMoq.It.is(a => argsMatch(args, a)), + TypeMoq.It.isAny() + ) + ) .returns(() => result); - } } - function setupPythonServiceWithFunc(service: TypeMoq.IMock, module: string, args: (string | RegExp)[], result: () => Promise>) { - service.setup(x => x.execModule( - TypeMoq.It.isValue(module), - TypeMoq.It.is(a => argsMatch(args, a)), - TypeMoq.It.isAny())) + function setupPythonServiceWithFunc( + service: TypeMoq.IMock, + module: string, + args: (string | RegExp)[], + result: () => Promise> + ) { + service + .setup(x => + x.execModule( + TypeMoq.It.isValue(module), + TypeMoq.It.is(a => argsMatch(args, a)), + TypeMoq.It.isAny() + ) + ) .returns(result); const withModuleArgs = ['-m', module, ...args]; - service.setup(x => x.exec( - TypeMoq.It.is(a => argsMatch(withModuleArgs, a)), - TypeMoq.It.isAny())) + service + .setup(x => + x.exec( + TypeMoq.It.is(a => argsMatch(withModuleArgs, a)), + TypeMoq.It.isAny() + ) + ) .returns(result); - service.setup(x => x.execModule( - TypeMoq.It.isValue(module), - TypeMoq.It.is(a => argsMatch(args, a)), - TypeMoq.It.isAny())) + service + .setup(x => + x.execModule( + TypeMoq.It.isValue(module), + TypeMoq.It.is(a => argsMatch(args, a)), + TypeMoq.It.isAny() + ) + ) .returns(result); } @@ -428,31 +446,47 @@ suite('Jupyter Execution', async () => { } }; - service.setup(x => x.execModuleObservable( - TypeMoq.It.isValue(module), - TypeMoq.It.is(a => argsMatch(args, a)), - TypeMoq.It.isAny())) + service + .setup(x => + x.execModuleObservable( + TypeMoq.It.isValue(module), + TypeMoq.It.is(a => argsMatch(args, a)), + TypeMoq.It.isAny() + ) + ) .returns(() => result); const withModuleArgs = ['-m', module, ...args]; - service.setup(x => x.execObservable( - TypeMoq.It.is(a => argsMatch(withModuleArgs, a)), - TypeMoq.It.isAny())) + service + .setup(x => + x.execObservable( + TypeMoq.It.is(a => argsMatch(withModuleArgs, a)), + TypeMoq.It.isAny() + ) + ) .returns(() => result); } function setupProcessServiceExec(service: TypeMoq.IMock, file: string, args: (string | RegExp)[], result: Promise>) { - service.setup(x => x.exec( - TypeMoq.It.isValue(file), - TypeMoq.It.is(a => argsMatch(args, a)), - TypeMoq.It.isAny())) + service + .setup(x => + x.exec( + TypeMoq.It.isValue(file), + TypeMoq.It.is(a => argsMatch(args, a)), + TypeMoq.It.isAny() + ) + ) .returns(() => result); } function setupProcessServiceExecWithFunc(service: TypeMoq.IMock, file: string, args: (string | RegExp)[], result: () => Promise>) { - service.setup(x => x.exec( - TypeMoq.It.isValue(file), - TypeMoq.It.is(a => argsMatch(args, a)), - TypeMoq.It.isAny())) + service + .setup(x => + x.exec( + TypeMoq.It.isValue(file), + TypeMoq.It.is(a => argsMatch(args, a)), + TypeMoq.It.isAny() + ) + ) .returns(result); } @@ -468,10 +502,14 @@ suite('Jupyter Execution', async () => { } }; - service.setup(x => x.execObservable( - TypeMoq.It.isValue(file), - TypeMoq.It.is(a => argsMatch(args, a)), - TypeMoq.It.isAny())) + service + .setup(x => + x.execObservable( + TypeMoq.It.isValue(file), + TypeMoq.It.is(a => argsMatch(args, a)), + TypeMoq.It.isAny() + ) + ) .returns(() => result); } @@ -500,14 +538,22 @@ suite('Jupyter Execution', async () => { setupPythonServiceWithFunc(service, 'jupyter', ['kernelspec', 'list', '--json'], () => { // Return different results after we install our kernel if (ipykernelInstallCount > 0) { - const kernelSpecs = createKernelSpecs([{ name: 'working', resourceDir: path.dirname(workingKernelSpec) }, { name: '0e8519db-0895-416c-96df-fa80131ecea0', resourceDir: 'C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0' }]); + const kernelSpecs = createKernelSpecs([ + { name: 'working', resourceDir: path.dirname(workingKernelSpec) }, + { name: '0e8519db-0895-416c-96df-fa80131ecea0', resourceDir: 'C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0' } + ]); return Promise.resolve({ stdout: JSON.stringify(kernelSpecs) }); } else { - const kernelSpecs = createKernelSpecs([{ name: '0e8519db-0895-416c-96df-fa80131ecea0', resourceDir: 'C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0' }]); + const kernelSpecs = createKernelSpecs([ + { name: '0e8519db-0895-416c-96df-fa80131ecea0', resourceDir: 'C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0' } + ]); return Promise.resolve({ stdout: JSON.stringify(kernelSpecs) }); } }); - const kernelSpecs2 = createKernelSpecs([{ name: 'working', resourceDir: path.dirname(workingKernelSpec) }, { name: '0e8519db-0895-416c-96df-fa80131ecea0', resourceDir: 'C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0' }]); + const kernelSpecs2 = createKernelSpecs([ + { name: 'working', resourceDir: path.dirname(workingKernelSpec) }, + { name: '0e8519db-0895-416c-96df-fa80131ecea0', resourceDir: 'C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0' } + ]); setupPythonService(service, 'jupyter', ['kernelspec', 'list', '--json'], Promise.resolve({ stdout: JSON.stringify(kernelSpecs2) })); setupPythonServiceWithFunc(service, 'ipykernel', ['install', '--user', '--name', /\w+-\w+-\w+-\w+-\w+/, '--display-name', `'Python Interactive'`], () => { ipykernelInstallCount += 1; @@ -518,7 +564,13 @@ suite('Jupyter Execution', async () => { setupPythonService(service, undefined, [getServerInfoPath], Promise.resolve({ stdout: 'failure to get server infos' })); setupPythonServiceExecObservable(service, 'jupyter', ['kernelspec', 'list', '--json'], [], []); const dockerArgs = runInDocker ? ['--ip', '127.0.0.1'] : []; - setupPythonServiceExecObservable(service, 'jupyter', ['notebook', '--no-browser', /--notebook-dir=.*/, /--config=.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0', ...dockerArgs], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); + setupPythonServiceExecObservable( + service, + 'jupyter', + ['notebook', '--no-browser', /--notebook-dir=.*/, /--config=.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0', ...dockerArgs], + [], + notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198'] + ); } function setupMissingKernelPythonService(service: TypeMoq.IMock, notebookStdErr?: string[]) { @@ -530,12 +582,19 @@ suite('Jupyter Execution', async () => { const getServerInfoPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getServerInfo.py'); setupPythonService(service, undefined, [getServerInfoPath], Promise.resolve({ stdout: 'failure to get server infos' })); setupPythonServiceExecObservable(service, 'jupyter', ['kernelspec', 'list', '--json'], [], []); - setupPythonServiceExecObservable(service, 'jupyter', ['notebook', '--no-browser', /--notebook-dir=.*/, /--config=.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); + setupPythonServiceExecObservable( + service, + 'jupyter', + ['notebook', '--no-browser', /--notebook-dir=.*/, /--config=.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], + [], + notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198'] + ); } function setupMissingNotebookPythonService(service: TypeMoq.IMock) { - service.setup(x => x.execModule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns((_v) => { + service + .setup(x => x.execModule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(_v => { return Promise.reject('cant exec'); }); service.setup(x => x.getInterpreterInformation()).returns(() => Promise.resolve(missingNotebookPython)); @@ -546,24 +605,43 @@ suite('Jupyter Execution', async () => { setupProcessServiceExecWithFunc(service, workingPython.path, ['-m', 'jupyter', 'kernelspec', 'list', '--json'], () => { // Return different results after we install our kernel if (ipykernelInstallCount > 0) { - const kernelSpecs = createKernelSpecs([{ name: 'working', resourceDir: path.dirname(workingKernelSpec) }, { name: '0e8519db-0895-416c-96df-fa80131ecea0', resourceDir: 'C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0' }]); + const kernelSpecs = createKernelSpecs([ + { name: 'working', resourceDir: path.dirname(workingKernelSpec) }, + { name: '0e8519db-0895-416c-96df-fa80131ecea0', resourceDir: 'C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0' } + ]); return Promise.resolve({ stdout: JSON.stringify(kernelSpecs) }); } else { - const kernelSpecs = createKernelSpecs([{ name: '0e8519db-0895-416c-96df-fa80131ecea0', resourceDir: 'C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0' }]); + const kernelSpecs = createKernelSpecs([ + { name: '0e8519db-0895-416c-96df-fa80131ecea0', resourceDir: 'C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0' } + ]); return Promise.resolve({ stdout: JSON.stringify(kernelSpecs) }); } }); - const kernelSpecs2 = createKernelSpecs([{ name: 'working', resourceDir: path.dirname(workingKernelSpec) }, { name: '0e8519db-0895-416c-96df-fa80131ecea0', resourceDir: 'C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0' }]); + const kernelSpecs2 = createKernelSpecs([ + { name: 'working', resourceDir: path.dirname(workingKernelSpec) }, + { name: '0e8519db-0895-416c-96df-fa80131ecea0', resourceDir: 'C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0' } + ]); setupProcessServiceExec(service, workingPython.path, ['-m', 'jupyter', 'kernelspec', 'list', '--json'], Promise.resolve({ stdout: JSON.stringify(kernelSpecs2) })); - setupProcessServiceExecWithFunc(service, workingPython.path, ['-m', 'ipykernel', 'install', '--user', '--name', /\w+-\w+-\w+-\w+-\w+/, '--display-name', `'Python Interactive'`], () => { - ipykernelInstallCount += 1; - const kernelSpecs = createKernelSpecs([{ name: 'somename', resourceDir: path.dirname(workingKernelSpec) }]); - return Promise.resolve({ stdout: JSON.stringify(kernelSpecs) }); - }); + setupProcessServiceExecWithFunc( + service, + workingPython.path, + ['-m', 'ipykernel', 'install', '--user', '--name', /\w+-\w+-\w+-\w+-\w+/, '--display-name', `'Python Interactive'`], + () => { + ipykernelInstallCount += 1; + const kernelSpecs = createKernelSpecs([{ name: 'somename', resourceDir: path.dirname(workingKernelSpec) }]); + return Promise.resolve({ stdout: JSON.stringify(kernelSpecs) }); + } + ); const getServerInfoPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getServerInfo.py'); setupProcessServiceExec(service, workingPython.path, [getServerInfoPath], Promise.resolve({ stdout: 'failure to get server infos' })); setupProcessServiceExecObservable(service, workingPython.path, ['-m', 'jupyter', 'kernelspec', 'list', '--json'], [], []); - setupProcessServiceExecObservable(service, workingPython.path, ['-m', 'jupyter', 'notebook', '--no-browser', /--notebook-dir=.*/, /--config=.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); + setupProcessServiceExecObservable( + service, + workingPython.path, + ['-m', 'jupyter', 'notebook', '--no-browser', /--notebook-dir=.*/, /--config=.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], + [], + notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198'] + ); } function setupMissingKernelProcessService(service: TypeMoq.IMock, notebookStdErr?: string[]) { @@ -572,7 +650,13 @@ suite('Jupyter Execution', async () => { const getServerInfoPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getServerInfo.py'); setupProcessServiceExec(service, missingKernelPython.path, [getServerInfoPath], Promise.resolve({ stdout: 'failure to get server infos' })); setupProcessServiceExecObservable(service, missingKernelPython.path, ['-m', 'jupyter', 'kernelspec', 'list', '--json'], [], []); - setupProcessServiceExecObservable(service, missingKernelPython.path, ['-m', 'jupyter', 'notebook', '--no-browser', /--notebook-dir=.*/, /--config=.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); + setupProcessServiceExecObservable( + service, + missingKernelPython.path, + ['-m', 'jupyter', 'notebook', '--no-browser', /--notebook-dir=.*/, /--config=.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], + [], + notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198'] + ); } function setupPathProcessService(jupyterPath: string, service: TypeMoq.IMock, notebookStdErr?: string[]) { @@ -582,7 +666,13 @@ suite('Jupyter Execution', async () => { setupProcessServiceExec(service, jupyterPath, ['--version'], Promise.resolve({ stdout: '1.1.1.1' })); setupProcessServiceExec(service, jupyterPath, ['notebook', '--version'], Promise.resolve({ stdout: '1.1.1.1' })); setupProcessServiceExec(service, jupyterPath, ['kernelspec', '--version'], Promise.resolve({ stdout: '1.1.1.1' })); - setupProcessServiceExecObservable(service, jupyterPath, ['notebook', '--no-browser', /--notebook-dir=.*/, /--config=.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); + setupProcessServiceExecObservable( + service, + jupyterPath, + ['notebook', '--no-browser', /--notebook-dir=.*/, /--config=.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], + [], + notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198'] + ); // WE also check for existence with just the key jupyter setupProcessServiceExec(service, 'jupyter', ['--version'], Promise.resolve({ stdout: '1.1.1.1' })); @@ -593,7 +683,12 @@ suite('Jupyter Execution', async () => { function createExecution(activeInterpreter: PythonInterpreter, notebookStdErr?: string[], skipSearch?: boolean): JupyterExecutionFactory { return createExecutionAndReturnProcessService(activeInterpreter, notebookStdErr, skipSearch).jupyterExecutionFactory; } - function createExecutionAndReturnProcessService(activeInterpreter: PythonInterpreter, notebookStdErr?: string[], skipSearch?: boolean, runInDocker?: boolean): { workingPythonExecutionService: TypeMoq.IMock; jupyterExecutionFactory: JupyterExecutionFactory } { + function createExecutionAndReturnProcessService( + activeInterpreter: PythonInterpreter, + notebookStdErr?: string[], + skipSearch?: boolean, + runInDocker?: boolean + ): { workingPythonExecutionService: TypeMoq.IMock; jupyterExecutionFactory: JupyterExecutionFactory } { // Setup defaults when(interpreterService.onDidChangeInterpreter).thenReturn(dummyEvent.event); when(interpreterService.getActiveInterpreter()).thenResolve(activeInterpreter); @@ -648,7 +743,9 @@ suite('Jupyter Execution', async () => { when(workspaceService.onDidChangeConfiguration).thenReturn(configChangeEvent.event); when(application.withProgress(anything(), anything())).thenCall((_, cb: (_: any, token: any) => Promise) => { return new Promise((resolve, reject) => { - cb({ report: noop }, new CancellationTokenSource().token).then(resolve).catch(reject); + cb({ report: noop }, new CancellationTokenSource().token) + .then(resolve) + .catch(reject); }); }); @@ -697,13 +794,11 @@ suite('Jupyter Execution', async () => { when(fileSystem.createDirectory(anything())).thenResolve(); when(fileSystem.deleteDirectory(anything())).thenResolve(); when(fileSystem.fileExists(workingKernelSpec)).thenResolve(true); - when(fileSystem.readFile(workingKernelSpec)).thenResolve('{"display_name":"Python 3","language":"python","argv":["/foo/bar/python.exe","-m","ipykernel_launcher","-f","{connection_file}"]}'); + when(fileSystem.readFile(workingKernelSpec)).thenResolve( + '{"display_name":"Python 3","language":"python","argv":["/foo/bar/python.exe","-m","ipykernel_launcher","-f","{connection_file}"]}' + ); - const commandFactory = new JupyterCommandFactory( - instance(executionFactory), - instance(activationHelper), - instance(processServiceFactory), - instance(interpreterService)); + const commandFactory = new JupyterCommandFactory(instance(executionFactory), instance(activationHelper), instance(processServiceFactory), instance(interpreterService)); const persistentSateFactory = mock(PersistentStateFactory); const persistentState = mock(PersistentState); when(persistentState.updateValue(anything())).thenResolve(); @@ -723,7 +818,8 @@ suite('Jupyter Execution', async () => { commandFactory, instance(workspaceService), instance(application), - instance(persistentSateFactory)); + instance(persistentSateFactory) + ); when(serviceContainer.get(JupyterCommandFinder)).thenReturn(commandFinder); when(serviceContainer.get(IInterpreterService)).thenReturn(instance(interpreterService)); when(serviceContainer.get(IProcessServiceFactory)).thenReturn(instance(processServiceFactory)); @@ -738,7 +834,14 @@ suite('Jupyter Execution', async () => { path: '' }; when(kernelSelector.getKernelForLocalConnection(anything(), anything(), anything())).thenResolve({ kernelSpec }); - notebookStarter = new NotebookStarter(instance(executionFactory), commandFinder, instance(fileSystem), instance(serviceContainer), instance(interpreterService), instance(jupyterOutputChannel)); + notebookStarter = new NotebookStarter( + instance(executionFactory), + commandFinder, + instance(fileSystem), + instance(serviceContainer), + instance(interpreterService), + instance(jupyterOutputChannel) + ); when(serviceContainer.get(KernelSelector)).thenReturn(instance(kernelSelector)); when(serviceContainer.get(NotebookStarter)).thenReturn(notebookStarter); return { @@ -756,7 +859,8 @@ suite('Jupyter Execution', async () => { notebookStarter, instance(application), instance(jupyterOutputChannel), - instance(serviceContainer)) + instance(serviceContainer) + ) }; } @@ -797,13 +901,16 @@ suite('Jupyter Execution', async () => { test('Slow notebook startups throws exception', async () => { const daemonService = mock(PythonDaemonExecutionService); const stdErr = 'Failure'; - const proc = { on: noop } as any as ChildProcess; + const proc = ({ on: noop } as any) as ChildProcess; const out = new Observable>(s => s.next({ source: 'stderr', out: stdErr })); when(daemonService.execModuleObservable(anything(), anything(), anything())).thenReturn({ dispose: noop, proc: proc, out }); when(executionFactory.createDaemon(deepEqual({ daemonModule: PythonDaemonModule, pythonPath: workingPython.path }))).thenResolve(instance(daemonService)); const execution = createExecution(workingPython, [stdErr]); - await assert.isRejected(execution.connectToNotebookServer(), `Jupyter notebook failed to launch. \r\nError: The Jupyter notebook server failed to launch in time\n${stdErr}`); + await assert.isRejected( + execution.connectToNotebookServer(), + `Jupyter notebook failed to launch. \r\nError: The Jupyter notebook server failed to launch in time\n${stdErr}` + ); }).timeout(10000); test('Other than active works', async () => { @@ -823,7 +930,8 @@ suite('Jupyter Execution', async () => { await assert.eventually.equal(execution.isNotebookSupported(), true, 'Notebook not supported'); const usableInterpreter = await execution.getUsableJupyterPython(); assert.isOk(usableInterpreter, 'Usable interpreter not found'); - if (usableInterpreter) { // Linter + if (usableInterpreter) { + // Linter assert.equal(usableInterpreter.path, missingKernelPython.path); assert.equal(usableInterpreter.version!.major, missingKernelPython.version!.major, 'Found interpreter should match on major'); assert.equal(usableInterpreter.version!.minor, missingKernelPython.version!.minor, 'Found interpreter should match on minor'); @@ -836,7 +944,8 @@ suite('Jupyter Execution', async () => { await assert.eventually.equal(execution.isNotebookSupported(), true, 'Notebook not supported'); const usableInterpreter = await execution.getUsableJupyterPython(); assert.isOk(usableInterpreter, 'Usable interpreter not found'); - if (usableInterpreter) { // Linter + if (usableInterpreter) { + // Linter assert.notEqual(usableInterpreter.path, missingNotebookPython.path); assert.notEqual(usableInterpreter.version!.major, missingNotebookPython.version!.major, 'Found interpreter should not match on major'); } @@ -858,7 +967,8 @@ suite('Jupyter Execution', async () => { await assert.eventually.equal(execution.isNotebookSupported(), true, 'Notebook not supported'); const usableInterpreter = await execution.getUsableJupyterPython(); assert.isOk(usableInterpreter, 'Usable interpreter not found'); - if (usableInterpreter) { // Linter + if (usableInterpreter) { + // Linter assert.notEqual(usableInterpreter.path, missingNotebookPython.path); assert.notEqual(usableInterpreter.version!.major, missingNotebookPython.version!.major, 'Found interpreter should not match on major'); } @@ -882,7 +992,9 @@ suite('Jupyter Execution', async () => { reset(application); when(application.withProgress(anything(), anything())).thenCall((_, cb: (_: any, token: any) => Promise) => { return new Promise((resolve, reject) => { - cb({ report: noop }, progressCancellation.token).then(resolve).catch(reject); + cb({ report: noop }, progressCancellation.token) + .then(resolve) + .catch(reject); }); }); @@ -901,7 +1013,9 @@ suite('Jupyter Execution', async () => { reset(application); when(application.withProgress(anything(), anything())).thenCall((_, cb: (_: any, token: any) => Promise) => { return new Promise((resolve, reject) => { - cb({ report: noop }, progressCancellation.token).then(resolve).catch(reject); + cb({ report: noop }, progressCancellation.token) + .then(resolve) + .catch(reject); }); }); diff --git a/src/test/datascience/jupyter/kernels/kernelSelections.unit.test.ts b/src/test/datascience/jupyter/kernels/kernelSelections.unit.test.ts index 64f66e3cef13..dbfc2fe298b8 100644 --- a/src/test/datascience/jupyter/kernels/kernelSelections.unit.test.ts +++ b/src/test/datascience/jupyter/kernels/kernelSelections.unit.test.ts @@ -76,6 +76,7 @@ suite('Data Science - KernelSelections', () => { test('Should return an empty list for remote kernels if there are none', async () => { when(kernelService.getKernelSpecs(instance(sessionManager), anything())).thenResolve([]); when(sessionManager.getRunningKernels()).thenResolve([]); + when(sessionManager.getRunningSessions()).thenResolve([]); const items = await kernelSelectionProvider.getKernelSelectionsForRemoteSession(instance(sessionManager)); @@ -83,9 +84,19 @@ suite('Data Science - KernelSelections', () => { }); test('Should return a list with the proper details in the quick pick for remote connections (excluding non-python kernels)', async () => { const activeKernels: IJupyterKernel[] = [activePython1KernelModel, activeJuliaKernelModel]; - + const sessions = activeKernels.map(item => { + return { + id: 'sessionId', + name: 'someSession', + // tslint:disable-next-line: no-any + kernel: item as any, + type: '', + path: '' + }; + }); when(kernelService.getKernelSpecs(instance(sessionManager), anything())).thenResolve([]); when(sessionManager.getRunningKernels()).thenResolve(activeKernels); + when(sessionManager.getRunningSessions()).thenResolve(sessions); when(sessionManager.getKernelSpecs()).thenResolve(allSpecs); // Quick pick must contain @@ -95,7 +106,24 @@ suite('Data Science - KernelSelections', () => { const expectedItems: IKernelSpecQuickPickItem[] = [ { label: python1KernelSpecModel.display_name, - selection: { interpreter: undefined, kernelModel: { ...activePython1KernelModel, ...python1KernelSpecModel }, kernelSpec: undefined }, + // tslint:disable-next-line: no-any + selection: { + interpreter: undefined, + kernelModel: { + ...activePython1KernelModel, + ...python1KernelSpecModel, + session: { + id: 'sessionId', + name: 'someSession', + // tslint:disable-next-line: no-any + kernel: activeKernels[0] as any, + type: '', + path: '' + // tslint:disable-next-line: no-any + } as any + }, + kernelSpec: undefined + }, detail: '', description: localize.DataScience.jupyterSelectURIRunningDetailFormat().format( activePython1KernelModel.lastActivityTime.toLocaleString(), diff --git a/src/test/datascience/jupyter/kernels/kernelSelector.unit.test.ts b/src/test/datascience/jupyter/kernels/kernelSelector.unit.test.ts index 5bcb49a5afb9..822e69f12d03 100644 --- a/src/test/datascience/jupyter/kernels/kernelSelector.unit.test.ts +++ b/src/test/datascience/jupyter/kernels/kernelSelector.unit.test.ts @@ -18,8 +18,8 @@ import { JupyterSessionManager } from '../../../../client/datascience/jupyter/ju import { KernelSelectionProvider } from '../../../../client/datascience/jupyter/kernels/kernelSelections'; import { KernelSelector } from '../../../../client/datascience/jupyter/kernels/kernelSelector'; import { KernelService } from '../../../../client/datascience/jupyter/kernels/kernelService'; -import { IKernelSpecQuickPickItem } from '../../../../client/datascience/jupyter/kernels/types'; -import { IJupyterKernel, IJupyterKernelSpec, IJupyterSessionManager } from '../../../../client/datascience/types'; +import { IKernelSpecQuickPickItem, LiveKernelModel } from '../../../../client/datascience/jupyter/kernels/types'; +import { IJupyterKernelSpec, IJupyterSessionManager } from '../../../../client/datascience/types'; import { IInterpreterService, InterpreterType, PythonInterpreter } from '../../../../client/interpreter/contracts'; import { InterpreterService } from '../../../../client/interpreter/interpreterService'; @@ -103,11 +103,15 @@ suite('Data Science - KernelSelector', () => { }); suite('Hide kernels from Remote & Local Kernel', () => { test('Should hide kernel from remote sessions', async () => { - const kernelModels: (IJupyterKernel & Partial)[] = [ - {lastActivityTime: new Date(), name: '1one', numberOfConnections: 1, id: 'id1', display_name: '1'}, - {lastActivityTime: new Date(), name: '2two', numberOfConnections: 1, id: 'id2', display_name: '2'}, - {lastActivityTime: new Date(), name: '3three', numberOfConnections: 1, id: 'id3', display_name: '3'}, - {lastActivityTime: new Date(), name: '4four', numberOfConnections: 1, id: 'id4', display_name: '4'} + const kernelModels: (LiveKernelModel)[] = [ + // tslint:disable-next-line: no-any + {lastActivityTime: new Date(), name: '1one', numberOfConnections: 1, id: 'id1', display_name: '1', session: {} as any}, + // tslint:disable-next-line: no-any + {lastActivityTime: new Date(), name: '2two', numberOfConnections: 1, id: 'id2', display_name: '2', session: {} as any}, + // tslint:disable-next-line: no-any + {lastActivityTime: new Date(), name: '3three', numberOfConnections: 1, id: 'id3', display_name: '3', session: {} as any}, + // tslint:disable-next-line: no-any + {lastActivityTime: new Date(), name: '4four', numberOfConnections: 1, id: 'id4', display_name: '4', session: {} as any} ]; const quickPickItems: IKernelSpecQuickPickItem[] = kernelModels.map(kernelModel => { return { @@ -132,11 +136,15 @@ suite('Data Science - KernelSelector', () => { assert.deepEqual(suggestions, quickPickItems.filter(item => !['id2', 'id4'].includes(item.selection?.kernelModel?.id || ''))); }); test('Should hide kernel from local sessions', async () => { - const kernelModels: (IJupyterKernel & Partial)[] = [ - {lastActivityTime: new Date(), name: '1one', numberOfConnections: 1, id: 'id1', display_name: '1'}, - {lastActivityTime: new Date(), name: '2two', numberOfConnections: 1, id: 'id2', display_name: '2'}, - {lastActivityTime: new Date(), name: '3three', numberOfConnections: 1, id: 'id3', display_name: '3'}, - {lastActivityTime: new Date(), name: '4four', numberOfConnections: 1, id: 'id4', display_name: '4'} + const kernelModels: (LiveKernelModel)[] = [ + // tslint:disable-next-line: no-any + {lastActivityTime: new Date(), name: '1one', numberOfConnections: 1, id: 'id1', display_name: '1', session: {} as any}, + // tslint:disable-next-line: no-any + {lastActivityTime: new Date(), name: '2two', numberOfConnections: 1, id: 'id2', display_name: '2', session: {} as any}, + // tslint:disable-next-line: no-any + {lastActivityTime: new Date(), name: '3three', numberOfConnections: 1, id: 'id3', display_name: '3', session: {} as any}, + // tslint:disable-next-line: no-any + {lastActivityTime: new Date(), name: '4four', numberOfConnections: 1, id: 'id4', display_name: '4', session: {} as any} ]; const quickPickItems: IKernelSpecQuickPickItem[] = kernelModels.map(kernelModel => { return { @@ -242,8 +250,11 @@ suite('Data Science - KernelSelector', () => { let nbMetadataKernelSpec: nbformat.IKernelspecMetadata = {} as any; // tslint:disable-next-line: no-any let nbMetadata: nbformat.INotebookMetadata = {} as any; + let selectLocalKernelStub: sinon.SinonStub< + [(IJupyterSessionManager | undefined)?, (CancellationToken | undefined)?, (IJupyterKernelSpec | LiveKernelModel)?], // tslint:disable-next-line: no-any - let selectLocalKernelStub: sinon.SinonStub<[(IJupyterSessionManager | undefined)?, (CancellationToken | undefined)?, (IJupyterKernelSpec | IJupyterKernel & Partial)?], Promise>; + Promise + >; setup(() => { nbMetadataKernelSpec = { display_name: interpreter.displayName!, diff --git a/src/test/datascience/mockJupyterManager.ts b/src/test/datascience/mockJupyterManager.ts index 99b7f41f6877..03b79c93df26 100644 --- a/src/test/datascience/mockJupyterManager.ts +++ b/src/test/datascience/mockJupyterManager.ts @@ -13,6 +13,7 @@ import * as uuid from 'uuid/v4'; import { EventEmitter, Uri } from 'vscode'; import { CancellationToken } from 'vscode-jsonrpc'; +import { Session } from '@jupyterlab/services'; import { Cancellation } from '../../client/common/cancellation'; import { ExecutionResult, IProcessServiceFactory, IPythonExecutionFactory, Output } from '../../client/common/process/types'; import { IConfigurationService } from '../../client/common/types'; @@ -165,6 +166,10 @@ export class MockJupyterManager implements IJupyterSessionManager { return Promise.resolve([]); } + public getRunningSessions(): Promise { + return Promise.resolve([]); + } + public setProcessDelay(timeout: number | undefined) { this.processService.setDelay(timeout); this.pythonServices.forEach(p => p.setDelay(timeout)); diff --git a/src/test/datascience/mockJupyterSession.ts b/src/test/datascience/mockJupyterSession.ts index 9a716d92f011..fbf9dea1c7eb 100644 --- a/src/test/datascience/mockJupyterSession.ts +++ b/src/test/datascience/mockJupyterSession.ts @@ -6,7 +6,8 @@ import { JSONObject } from '@phosphor/coreutils/lib/json'; import { CancellationTokenSource, Event, EventEmitter } from 'vscode'; import { JupyterKernelPromiseFailedError } from '../../client/datascience/jupyter/kernels/jupyterKernelPromiseFailedError'; -import { ICell, IJupyterKernel, IJupyterKernelSpec, IJupyterSession } from '../../client/datascience/types'; +import { LiveKernelModel } from '../../client/datascience/jupyter/kernels/types'; +import { ICell, IJupyterKernelSpec, IJupyterSession } from '../../client/datascience/types'; import { ServerStatus } from '../../datascience-ui/interactive-common/mainState'; import { sleep } from '../core'; import { MockJupyterRequest } from './mockJupyterRequest'; @@ -115,10 +116,8 @@ export class MockJupyterSession implements IJupyterSession { msg_id: '1', msg_type: 'complete' }, - parent_header: { - }, - metadata: { - } + parent_header: {}, + metadata: {} } as any; } @@ -134,7 +133,7 @@ export class MockJupyterSession implements IJupyterSession { this.completionTimeout = timeout; } - public changeKernel(_kernel: IJupyterKernelSpec | IJupyterKernel & Partial): Promise { + public changeKernel(_kernel: IJupyterKernelSpec | LiveKernelModel): Promise { return Promise.resolve(); }