diff --git a/news/2 Fixes/9177.md b/news/2 Fixes/9177.md new file mode 100644 index 000000000000..cd241c5851b8 --- /dev/null +++ b/news/2 Fixes/9177.md @@ -0,0 +1 @@ +Jupyter output tab was not showing anything when connecting to a remote server. \ No newline at end of file diff --git a/package.nls.json b/package.nls.json index a0f3388551bf..bba360f9c0c2 100644 --- a/package.nls.json +++ b/package.nls.json @@ -442,5 +442,8 @@ "DataScience.gettingListOfKernelSpecs": "Fetching Kernel specs", "DataScience.startingJupyterNotebook": "Starting Jupyter Notebook", "DataScience.registeringKernel": "Registering Kernel", - "DataScience.trimmedOutput": "Output was trimmed for performance reasons.\nTo see the full output set the setting \"python.dataScience.textOutputLimit\" to 0." + "DataScience.trimmedOutput": "Output was trimmed for performance reasons.\nTo see the full output set the setting \"python.dataScience.textOutputLimit\" to 0.", + "DataScience.connectingToJupyterUri" : "Connecting to Jupyter server at {0}", + "DataScience.createdNewNotebook" : "{0}: Creating new notebook ", + "DataScience.createdNewKernel" : "{0}: Kernel started: {1}" } diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index f1f1951f9641..f5cb663db8c8 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -790,6 +790,14 @@ export namespace DataScience { 'DataScience.jupyterCommandLinePrompt', 'Enter your custom command line for Jupyter' ); + + export const connectingToJupyterUri = localize( + 'DataScience.connectingToJupyterUri', + 'Connecting to Jupyter server at {0}' + ); + export const createdNewNotebook = localize('DataScience.createdNewNotebook', '{0}: Creating new notebook '); + + export const createdNewKernel = localize('DataScience.createdNewKernel', '{0}: Kernel started: {1}'); } export namespace DebugConfigStrings { diff --git a/src/client/datascience/jupyter/jupyterServer.ts b/src/client/datascience/jupyter/jupyterServer.ts index 60b2a383ab97..49f4fad38caa 100644 --- a/src/client/datascience/jupyter/jupyterServer.ts +++ b/src/client/datascience/jupyter/jupyterServer.ts @@ -8,7 +8,13 @@ import { CancellationToken } from 'vscode-jsonrpc'; import { ILiveShareApi } from '../../common/application/types'; import '../../common/extensions'; import { traceError, traceInfo } from '../../common/logger'; -import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry, Resource } from '../../common/types'; +import { + IAsyncDisposableRegistry, + IConfigurationService, + IDisposableRegistry, + IOutputChannel, + Resource +} from '../../common/types'; import { createDeferred, Deferred } from '../../common/utils/async'; import * as localize from '../../common/utils/localize'; import { noop } from '../../common/utils/misc'; @@ -42,7 +48,8 @@ export class JupyterServerBase implements INotebookServer { private disposableRegistry: IDisposableRegistry, protected readonly configService: IConfigurationService, private sessionManagerFactory: IJupyterSessionManagerFactory, - private loggers: INotebookExecutionLogger[] + private loggers: INotebookExecutionLogger[], + private jupyterOutputChannel: IOutputChannel ) { this.asyncRegistry.push(this); } @@ -67,6 +74,9 @@ export class JupyterServerBase implements INotebookServer { }); } + // Indicate we have a new session on the output channel + this.logRemoteOutput(localize.DataScience.connectingToJupyterUri().format(launchInfo.connectionInfo.baseUrl)); + // Create our session manager this.sessionManager = await this.sessionManagerFactory.create(launchInfo.connectionInfo); // Try creating a session just to ensure we're connected. Callers of this function check to make sure jupyter @@ -104,7 +114,11 @@ export class JupyterServerBase implements INotebookServer { this.loggers, notebookMetadata, cancelToken - ); + ).then(r => { + const baseUrl = this.launchInfo?.connectionInfo.baseUrl || ''; + this.logRemoteOutput(localize.DataScience.createdNewNotebook().format(baseUrl)); + return r; + }); } public async shutdown(): Promise { @@ -217,4 +231,10 @@ export class JupyterServerBase implements INotebookServer { this.launchInfo.kernelSpec = undefined; } } + + private logRemoteOutput(output: string) { + if (this.launchInfo && !this.launchInfo.connectionInfo.localLaunch) { + this.jupyterOutputChannel.appendLine(output); + } + } } diff --git a/src/client/datascience/jupyter/jupyterServerWrapper.ts b/src/client/datascience/jupyter/jupyterServerWrapper.ts index 3d121f9aaf7c..b9c9a0643902 100644 --- a/src/client/datascience/jupyter/jupyterServerWrapper.ts +++ b/src/client/datascience/jupyter/jupyterServerWrapper.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. 'use strict'; import { nbformat } from '@jupyterlab/coreutils'; -import { inject, injectable, multiInject, optional } from 'inversify'; +import { inject, injectable, multiInject, named, optional } from 'inversify'; import * as uuid from 'uuid/v4'; import { Uri } from 'vscode'; import { CancellationToken } from 'vscode-jsonrpc'; @@ -10,8 +10,15 @@ import * as vsls from 'vsls/vscode'; import { IApplicationShell, ILiveShareApi, IWorkspaceService } from '../../common/application/types'; import '../../common/extensions'; import { IFileSystem } from '../../common/platform/types'; -import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry, Resource } from '../../common/types'; +import { + IAsyncDisposableRegistry, + IConfigurationService, + IDisposableRegistry, + IOutputChannel, + Resource +} from '../../common/types'; import { IInterpreterService } from '../../interpreter/contracts'; +import { JUPYTER_OUTPUT_CHANNEL } from '../constants'; import { IConnection, IDataScience, @@ -43,7 +50,8 @@ type JupyterServerClassType = { appShell: IApplicationShell, fs: IFileSystem, kernelSelector: KernelSelector, - interpreterService: IInterpreterService + interpreterService: IInterpreterService, + outputChannel: IOutputChannel ): IJupyterServerInterface; }; // tslint:enable:callable-types @@ -69,7 +77,8 @@ export class JupyterServerWrapper implements INotebookServer, ILiveShareHasRole @inject(IApplicationShell) appShell: IApplicationShell, @inject(IFileSystem) fs: IFileSystem, @inject(IInterpreterService) interpreterService: IInterpreterService, - @inject(KernelSelector) kernelSelector: KernelSelector + @inject(KernelSelector) kernelSelector: KernelSelector, + @inject(IOutputChannel) @named(JUPYTER_OUTPUT_CHANNEL) jupyterOutput: IOutputChannel ) { // The server factory will create the appropriate HostJupyterServer or GuestJupyterServer based on // the liveshare state. @@ -88,7 +97,8 @@ export class JupyterServerWrapper implements INotebookServer, ILiveShareHasRole appShell, fs, kernelSelector, - interpreterService + interpreterService, + jupyterOutput ); } diff --git a/src/client/datascience/jupyter/jupyterSession.ts b/src/client/datascience/jupyter/jupyterSession.ts index 5b14719506bc..b206719b99bf 100644 --- a/src/client/datascience/jupyter/jupyterSession.ts +++ b/src/client/datascience/jupyter/jupyterSession.ts @@ -19,6 +19,7 @@ import { ServerStatus } from '../../../datascience-ui/interactive-common/mainSta import { Cancellation } from '../../common/cancellation'; import { isTestExecution } from '../../common/constants'; import { traceInfo, traceWarning } from '../../common/logger'; +import { IOutputChannel } from '../../common/types'; import { sleep, waitForPromise } from '../../common/utils/async'; import * as localize from '../../common/utils/localize'; import { noop } from '../../common/utils/misc'; @@ -69,7 +70,8 @@ export class JupyterSession implements IJupyterSession { private kernelSpec: IJupyterKernelSpec | LiveKernelModel | undefined, private sessionManager: SessionManager, private contentsManager: ContentsManager, - private readonly kernelSelector: KernelSelector + private readonly kernelSelector: KernelSelector, + private readonly outputChannel: IOutputChannel ) { this.statusHandler = this.onStatusChanged.bind(this); } @@ -386,11 +388,25 @@ export class JupyterSession implements IJupyterSession { }; return Cancellation.race( - () => this.sessionManager!.startNew(options).catch(ex => Promise.reject(new JupyterSessionStartError(ex))), + () => + this.sessionManager!.startNew(options) + .then(s => { + this.logRemoteOutput( + localize.DataScience.createdNewKernel().format(this.connInfo.baseUrl, s.kernel.id) + ); + return s; + }) + .catch(ex => Promise.reject(new JupyterSessionStartError(ex))), cancelToken ); } + private logRemoteOutput(output: string) { + if (this.connInfo && !this.connInfo.localLaunch) { + this.outputChannel.appendLine(output); + } + } + private async waitForKernelPromise( kernelPromise: Promise, timeout: number, diff --git a/src/client/datascience/jupyter/jupyterSessionManager.ts b/src/client/datascience/jupyter/jupyterSessionManager.ts index bdd1b1dc29be..a2faf3eb0779 100644 --- a/src/client/datascience/jupyter/jupyterSessionManager.ts +++ b/src/client/datascience/jupyter/jupyterSessionManager.ts @@ -6,7 +6,7 @@ import { Agent as HttpsAgent } from 'https'; import { CancellationToken } from 'vscode-jsonrpc'; import { traceInfo } from '../../common/logger'; -import { IConfigurationService } from '../../common/types'; +import { IConfigurationService, IOutputChannel } from '../../common/types'; import * as localize from '../../common/utils/localize'; import { IConnection, @@ -33,7 +33,8 @@ export class JupyterSessionManager implements IJupyterSessionManager { private jupyterPasswordConnect: IJupyterPasswordConnect, private config: IConfigurationService, private failOnPassword: boolean | undefined, - private kernelSelector: KernelSelector + private kernelSelector: KernelSelector, + private outputChannel: IOutputChannel ) {} public async dispose() { @@ -115,7 +116,8 @@ export class JupyterSessionManager implements IJupyterSessionManager { kernelSpec, this.sessionManager, this.contentsManager, - this.kernelSelector + this.kernelSelector, + this.outputChannel ); try { await session.connect(cancelToken); diff --git a/src/client/datascience/jupyter/jupyterSessionManagerFactory.ts b/src/client/datascience/jupyter/jupyterSessionManagerFactory.ts index 096d6a9c93b1..d9ac95af806e 100644 --- a/src/client/datascience/jupyter/jupyterSessionManagerFactory.ts +++ b/src/client/datascience/jupyter/jupyterSessionManagerFactory.ts @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. 'use strict'; -import { inject, injectable } from 'inversify'; +import { inject, injectable, named } from 'inversify'; -import { IConfigurationService } from '../../common/types'; +import { IConfigurationService, IOutputChannel } from '../../common/types'; +import { JUPYTER_OUTPUT_CHANNEL } from '../constants'; import { IConnection, IJupyterPasswordConnect, IJupyterSessionManager, IJupyterSessionManagerFactory } from '../types'; import { JupyterSessionManager } from './jupyterSessionManager'; import { KernelSelector } from './kernels/kernelSelector'; @@ -13,7 +14,8 @@ export class JupyterSessionManagerFactory implements IJupyterSessionManagerFacto constructor( @inject(IJupyterPasswordConnect) private jupyterPasswordConnect: IJupyterPasswordConnect, @inject(IConfigurationService) private config: IConfigurationService, - @inject(KernelSelector) private kernelSelector: KernelSelector + @inject(KernelSelector) private kernelSelector: KernelSelector, + @inject(IOutputChannel) @named(JUPYTER_OUTPUT_CHANNEL) private jupyterOutput: IOutputChannel ) {} /** @@ -26,7 +28,8 @@ export class JupyterSessionManagerFactory implements IJupyterSessionManagerFacto this.jupyterPasswordConnect, this.config, failOnPassword, - this.kernelSelector + this.kernelSelector, + this.jupyterOutput ); await result.initialize(connInfo); return result; diff --git a/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts b/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts index 8cd714a7bf19..b510f2be3e13 100644 --- a/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts +++ b/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts @@ -12,7 +12,13 @@ import { nbformat } from '@jupyterlab/coreutils'; import { IApplicationShell, ILiveShareApi, IWorkspaceService } from '../../../common/application/types'; import { traceInfo } from '../../../common/logger'; import { IFileSystem } from '../../../common/platform/types'; -import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry, Resource } from '../../../common/types'; +import { + IAsyncDisposableRegistry, + IConfigurationService, + IDisposableRegistry, + IOutputChannel, + Resource +} from '../../../common/types'; import * as localize from '../../../common/utils/localize'; import { IInterpreterService } from '../../../interpreter/contracts'; import { Identifiers, LiveShare, LiveShareCommands, RegExpValues } from '../../constants'; @@ -52,9 +58,10 @@ export class HostJupyterServer extends LiveShareParticipantHost(JupyterServerBas private appService: IApplicationShell, private fs: IFileSystem, private readonly kernelSelector: KernelSelector, - private readonly interpreterService: IInterpreterService + private readonly interpreterService: IInterpreterService, + outputChannel: IOutputChannel ) { - super(liveShare, asyncRegistry, disposableRegistry, configService, sessionManager, loggers); + super(liveShare, asyncRegistry, disposableRegistry, configService, sessionManager, loggers, outputChannel); } public async dispose(): Promise { diff --git a/src/test/datascience/jupyter/jupyterSession.unit.test.ts b/src/test/datascience/jupyter/jupyterSession.unit.test.ts index 61a147131f91..73933ca22de1 100644 --- a/src/test/datascience/jupyter/jupyterSession.unit.test.ts +++ b/src/test/datascience/jupyter/jupyterSession.unit.test.ts @@ -17,6 +17,7 @@ import { JupyterSession } from '../../../client/datascience/jupyter/jupyterSessi import { KernelSelector } from '../../../client/datascience/jupyter/kernels/kernelSelector'; import { LiveKernelModel } from '../../../client/datascience/jupyter/kernels/types'; import { IConnection, IJupyterKernelSpec } from '../../../client/datascience/types'; +import { MockOutputChannel } from '../../mockClasses'; // tslint:disable: max-func-body-length suite('Data Science - JupyterSession', () => { @@ -63,6 +64,7 @@ suite('Data Science - JupyterSession', () => { kernelChangedSignal = mock(Signal); when(session.statusChanged).thenReturn(instance(statusChangedSignal)); when(session.kernelChanged).thenReturn(instance(kernelChangedSignal)); + const channel = new MockOutputChannel('JUPYTER'); // tslint:disable-next-line: no-any (instance(session) as any).then = undefined; sessionManager = mock(SessionManager); @@ -73,7 +75,8 @@ suite('Data Science - JupyterSession', () => { kernelSpec.object, instance(sessionManager), instance(contentsManager), - instance(kernelSelector) + instance(kernelSelector), + channel ); });