Skip to content

Support launching with the same directory as a notebook #13324

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/2 Fixes/12760.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support starting kernels with the same directory as the notebook.
2 changes: 1 addition & 1 deletion src/client/datascience/baseJupyterSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export abstract class BaseJupyterSession implements IJupyterSession {
private _kernelSocket = new ReplaySubject<KernelSocketInformation | undefined>();
private _jupyterLab?: typeof import('@jupyterlab/services');

constructor(private restartSessionUsed: (id: Kernel.IKernelConnection) => void) {
constructor(private restartSessionUsed: (id: Kernel.IKernelConnection) => void, public workingDirectory: string) {
this.statusHandler = this.onStatusChanged.bind(this);
}
public dispose(): Promise<void> {
Expand Down
4 changes: 3 additions & 1 deletion src/client/datascience/jupyter/jupyterConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export class JupyterConnectionWaiter implements IDisposable {
constructor(
private readonly launchResult: ObservableExecutionResult<string>,
private readonly notebookDir: string,
private readonly rootDir: string,
private readonly getServerInfo: (cancelToken?: CancellationToken) => Promise<JupyterServerInfo[] | undefined>,
serviceContainer: IServiceContainer,
private cancelToken?: CancellationToken
Expand Down Expand Up @@ -105,7 +106,7 @@ export class JupyterConnectionWaiter implements IDisposable {

private createConnection(baseUrl: string, token: string, hostName: string, processDisposable: Disposable) {
// tslint:disable-next-line: no-use-before-declare
return new JupyterConnection(baseUrl, token, hostName, processDisposable, this.launchResult.proc);
return new JupyterConnection(baseUrl, token, hostName, this.rootDir, processDisposable, this.launchResult.proc);
}

// tslint:disable-next-line:no-any
Expand Down Expand Up @@ -230,6 +231,7 @@ class JupyterConnection implements IJupyterConnection {
public readonly baseUrl: string,
public readonly token: string,
public readonly hostName: string,
public readonly rootDirectory: string,
private readonly disposable: Disposable,
childProc: ChildProcess | undefined
) {
Expand Down
17 changes: 14 additions & 3 deletions src/client/datascience/jupyter/jupyterExecution.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
'use strict';
import * as path from 'path';
import * as uuid from 'uuid/v4';
import { CancellationToken, CancellationTokenSource, Event, EventEmitter, Uri } from 'vscode';

Expand Down Expand Up @@ -33,7 +34,7 @@ import {
JupyterServerUriHandle
} from '../types';
import { JupyterSelfCertsError } from './jupyterSelfCertsError';
import { createRemoteConnectionInfo } from './jupyterUtils';
import { createRemoteConnectionInfo, expandWorkingDir } from './jupyterUtils';
import { JupyterWaitForIdleError } from './jupyterWaitForIdleError';
import { KernelSelector, KernelSpecInterpreter } from './kernels/kernelSelector';
import { NotebookStarter } from './notebookStarter';
Expand All @@ -53,7 +54,7 @@ export class JupyterExecutionBase implements IJupyterExecution {
_liveShare: ILiveShareApi,
private readonly interpreterService: IInterpreterService,
private readonly disposableRegistry: IDisposableRegistry,
workspace: IWorkspaceService,
private readonly workspace: IWorkspaceService,
private readonly configuration: IConfigurationService,
private readonly kernelSelector: KernelSelector,
private readonly notebookStarter: NotebookStarter,
Expand Down Expand Up @@ -361,9 +362,18 @@ export class JupyterExecutionBase implements IJupyterExecution {
// If that works, then attempt to start the server
traceInfo(`Launching ${options ? options.purpose : 'unknown type of'} server`);
const useDefaultConfig = !options || options.skipUsingDefaultConfig ? false : true;

// Expand the working directory. Create a dummy launching file in the root path (so we expand correctly)
const workingDirectory = expandWorkingDir(
options?.workingDir,
this.workspace.rootPath ? path.join(this.workspace.rootPath, `${uuid()}.txt`) : undefined,
this.workspace
);

const connection = await this.startNotebookServer(
useDefaultConfig,
this.configuration.getSettings(undefined).datascience.jupyterCommandLineArguments,
workingDirectory,
cancelToken
);
if (connection) {
Expand All @@ -389,9 +399,10 @@ export class JupyterExecutionBase implements IJupyterExecution {
private async startNotebookServer(
useDefaultConfig: boolean,
customCommandLine: string[],
workingDirectory: string,
cancelToken?: CancellationToken
): Promise<IJupyterConnection> {
return this.notebookStarter.start(useDefaultConfig, customCommandLine, cancelToken);
return this.notebookStarter.start(useDefaultConfig, customCommandLine, workingDirectory, cancelToken);
}
private onSettingsChanged() {
// Clear our usableJupyterInterpreter so that we recompute our values
Expand Down
7 changes: 6 additions & 1 deletion src/client/datascience/jupyter/jupyterServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,15 @@ export class JupyterServerBase implements INotebookServer {

// 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
// is running and connectable.
let session: IJupyterSession | undefined;
session = await this.sessionManager.startNew(launchInfo.kernelSpec, cancelToken);
session = await this.sessionManager.startNew(
launchInfo.kernelSpec,
launchInfo.connectionInfo.rootDirectory,
cancelToken
);
const idleTimeout = this.configService.getSettings().datascience.jupyterLaunchTimeout;
// The wait for idle should throw if we can't connect.
await session.waitForIdle(idleTimeout);
Expand Down
32 changes: 26 additions & 6 deletions src/client/datascience/jupyter/jupyterSession.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
'use strict';
import type { ContentsManager, Kernel, ServerConnection, Session, SessionManager } from '@jupyterlab/services';
import type {
Contents,
ContentsManager,
Kernel,
ServerConnection,
Session,
SessionManager
} from '@jupyterlab/services';
import * as path from 'path';
import * as uuid from 'uuid/v4';
import { CancellationToken } from 'vscode-jsonrpc';
Expand Down Expand Up @@ -29,9 +36,10 @@ export class JupyterSession extends BaseJupyterSession {
private contentsManager: ContentsManager,
private readonly outputChannel: IOutputChannel,
private readonly restartSessionCreated: (id: Kernel.IKernelConnection) => void,
restartSessionUsed: (id: Kernel.IKernelConnection) => void
restartSessionUsed: (id: Kernel.IKernelConnection) => void,
readonly workingDirectory: string
) {
super(restartSessionUsed);
super(restartSessionUsed, workingDirectory);
this.kernelSpec = kernelSpec;
}

Expand Down Expand Up @@ -137,11 +145,23 @@ export class JupyterSession extends BaseJupyterSession {
contentsManager: ContentsManager,
cancelToken?: CancellationToken
): Promise<ISessionWithSocket> {
// First make sure the notebook is in the right relative path (jupyter expects a relative path with unix delimiters)
const relativeDirectory = path.relative(this.connInfo.rootDirectory, this.workingDirectory).replace(/\\/g, '/');

// However jupyter does not support relative paths outside of the original root.
const backingFileOptions: Contents.ICreateOptions =
this.connInfo.localLaunch && !relativeDirectory.startsWith('..')
? { type: 'notebook', path: relativeDirectory }
: { type: 'notebook' };

// Create a temporary notebook for this session. Each needs a unique name (otherwise we get the same session every time)
let backingFile = await contentsManager.newUntitled({ type: 'notebook' });
let backingFile = await contentsManager.newUntitled(backingFileOptions);
const backingFileDir = path.dirname(backingFile.path);
backingFile = await contentsManager.rename(
backingFile.path,
`${path.dirname(backingFile.path)}/t-${uuid()}.ipynb` // Note, the docs say the path uses UNIX delimiters.
backingFileDir.length && backingFileDir !== '.'
? `${backingFileDir}/t-${uuid()}.ipynb`
: `t-${uuid()}.ipynb` // Note, the docs say the path uses UNIX delimiters.
);

// Create our session options using this temporary notebook and our connection info
Expand Down Expand Up @@ -176,7 +196,7 @@ export class JupyterSession extends BaseJupyterSession {
})
.catch((ex) => Promise.reject(new JupyterSessionStartError(ex)))
.finally(() => {
if (this.connInfo && !this.connInfo.localLaunch) {
if (this.connInfo) {
this.contentsManager.delete(backingFile.path).ignoreErrors();
}
}),
Expand Down
4 changes: 3 additions & 1 deletion src/client/datascience/jupyter/jupyterSessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ export class JupyterSessionManager implements IJupyterSessionManager {

public async startNew(
kernelSpec: IJupyterKernelSpec | LiveKernelModel | undefined,
workingDirectory: string,
cancelToken?: CancellationToken
): Promise<IJupyterSession> {
if (!this.connInfo || !this.sessionManager || !this.contentsManager || !this.serverSettings) {
Expand All @@ -180,7 +181,8 @@ export class JupyterSessionManager implements IJupyterSessionManager {
this.contentsManager,
this.outputChannel,
this.restartSessionCreatedEvent.fire.bind(this.restartSessionCreatedEvent),
this.restartSessionUsedEvent.fire.bind(this.restartSessionUsedEvent)
this.restartSessionUsedEvent.fire.bind(this.restartSessionUsedEvent),
workingDirectory
);
try {
await session.connect(this.configService.getSettings().datascience.jupyterLaunchTimeout, cancelToken);
Expand Down
21 changes: 18 additions & 3 deletions src/client/datascience/jupyter/jupyterUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,30 @@ import { IJupyterConnection, IJupyterServerUri } from '../types';

export function expandWorkingDir(
workingDir: string | undefined,
launchingFile: string,
launchingFile: string | undefined,
workspace: IWorkspaceService
): string {
if (workingDir) {
const variables = new SystemVariables(Uri.file(launchingFile), undefined, workspace);
const variables = new SystemVariables(
launchingFile ? Uri.file(launchingFile) : undefined,
workspace.rootPath,
workspace
);
return variables.resolve(workingDir);
}

// No working dir, just use the path of the launching file.
return path.dirname(launchingFile);
if (launchingFile) {
return path.dirname(launchingFile);
}

// No launching file or working dir. Just use the default workspace folder
const workspaceFolder = workspace.getWorkspaceFolder(undefined);
if (workspaceFolder) {
return workspaceFolder.uri.fsPath;
}

return process.cwd();
}

export function createRemoteConnectionInfo(
Expand Down Expand Up @@ -59,6 +73,7 @@ export function createRemoteConnectionInfo(
return { dispose: noop };
},
dispose: noop,
rootDirectory: '',
getAuthHeader: serverUri ? () => getJupyterServerUri(uri)?.authorizationHeader : undefined
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ export class GuestJupyterSessionManager implements IJupyterSessionManager {
}
public startNew(
kernelSpec: IJupyterKernelSpec | LiveKernelModel | undefined,
workingDirectory: string,
cancelToken?: CancellationToken
): Promise<IJupyterSession> {
return this.realSessionManager.startNew(kernelSpec, cancelToken);
return this.realSessionManager.startNew(kernelSpec, workingDirectory, cancelToken);
}

public async getKernelSpecs(): Promise<IJupyterKernelSpec[]> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ export class HostJupyterExecution
disconnected: (_l) => {
return { dispose: noop };
},
dispose: noop
dispose: noop,
rootDirectory: connectionInfo.rootDirectory
};
}
}
Expand Down
14 changes: 12 additions & 2 deletions src/client/datascience/jupyter/liveshare/hostJupyterServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import '../../../common/extensions';
// tslint:disable-next-line: no-require-imports
import cloneDeep = require('lodash/cloneDeep');
import * as os from 'os';
import * as path from 'path';
import * as vscode from 'vscode';
import { CancellationToken } from 'vscode-jsonrpc';
import * as vsls from 'vsls/vscode';
Expand Down Expand Up @@ -215,8 +216,17 @@ export class HostJupyterServer extends LiveShareParticipantHost(JupyterServerBas
);
}

// Start a session (or use the existing one)
const session = possibleSession || (await sessionManager.startNew(info.kernelSpec, cancelToken));
// Figure out the working directory we need for our new notebook.
const workingDirectory =
resource && resource.scheme === 'file'
? path.dirname(resource.fsPath)
: this.workspaceService.getWorkspaceFolder(resource)?.uri.fsPath || process.cwd();

// Start a session (or use the existing one if allowed)
const session =
possibleSession && this.fs.areLocalPathsSame(possibleSession.workingDirectory, workingDirectory)
? possibleSession
: await sessionManager.startNew(info.kernelSpec, workingDirectory, cancelToken);
traceInfo(`Started session ${this.id}`);
return { info, session };
};
Expand Down
25 changes: 17 additions & 8 deletions src/client/datascience/jupyter/notebookStarter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export class NotebookStarter implements Disposable {
public async start(
useDefaultConfig: boolean,
customCommandLine: string[],
workingDirectory: string,
cancelToken?: CancellationToken
): Promise<IJupyterConnection> {
traceInfo('Starting Notebook');
Expand All @@ -71,7 +72,12 @@ export class NotebookStarter implements Disposable {
const tempDirPromise = this.generateTempDir();
tempDirPromise.then((dir) => this.disposables.push(dir)).ignoreErrors();
// Before starting the notebook process, make sure we generate a kernel spec
const args = await this.generateArguments(useDefaultConfig, customCommandLine, tempDirPromise);
const args = await this.generateArguments(
useDefaultConfig,
customCommandLine,
tempDirPromise,
workingDirectory
);

// Make sure we haven't canceled already.
if (cancelToken && cancelToken.isCancellationRequested) {
Expand Down Expand Up @@ -108,6 +114,7 @@ export class NotebookStarter implements Disposable {
starter = new JupyterConnectionWaiter(
launchResult,
tempDir.path,
workingDirectory,
this.jupyterInterpreterService.getRunningJupyterServers.bind(this.jupyterInterpreterService),
this.serviceContainer,
cancelToken
Expand Down Expand Up @@ -159,12 +166,13 @@ export class NotebookStarter implements Disposable {

private async generateDefaultArguments(
useDefaultConfig: boolean,
tempDirPromise: Promise<TemporaryDirectory>
tempDirPromise: Promise<TemporaryDirectory>,
workingDirectory: string
): Promise<string[]> {
// Parallelize as much as possible.
const promisedArgs: Promise<string>[] = [];
promisedArgs.push(Promise.resolve('--no-browser'));
promisedArgs.push(this.getNotebookDirArgument(tempDirPromise));
promisedArgs.push(Promise.resolve(this.getNotebookDirArgument(workingDirectory)));
if (useDefaultConfig) {
promisedArgs.push(this.getConfigArgument(tempDirPromise));
}
Expand Down Expand Up @@ -192,10 +200,11 @@ export class NotebookStarter implements Disposable {
private async generateArguments(
useDefaultConfig: boolean,
customCommandLine: string[],
tempDirPromise: Promise<TemporaryDirectory>
tempDirPromise: Promise<TemporaryDirectory>,
workingDirectory: string
): Promise<string[]> {
if (!customCommandLine || customCommandLine.length === 0) {
return this.generateDefaultArguments(useDefaultConfig, tempDirPromise);
return this.generateDefaultArguments(useDefaultConfig, tempDirPromise, workingDirectory);
}
return this.generateCustomArguments(customCommandLine);
}
Expand All @@ -208,9 +217,9 @@ export class NotebookStarter implements Disposable {
* @returns {Promise<void>}
* @memberof NotebookStarter
*/
private async getNotebookDirArgument(tempDirectory: Promise<TemporaryDirectory>): Promise<string> {
const tempDir = await tempDirectory;
return `--notebook-dir=${tempDir.path}`;
private getNotebookDirArgument(workingDirectory: string): string {
// Escape the result so Jupyter can actually read it.
return `--notebook-dir="${workingDirectory.replace(/\\/g, '\\\\')}"`;
}

/**
Expand Down
3 changes: 0 additions & 3 deletions src/client/datascience/kernel-launcher/kernelDaemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,6 @@ export class PythonKernelDaemon extends BasePythonDaemon implements IPythonKerne
if (options.mergeStdOutErr) {
throw new Error("'mergeStdOutErr' not supported in spawnOptions for KernelDaemon.start");
}
if (options.cwd) {
throw new Error("'cwd' not supported in spawnOptions for KernelDaemon.start");
}
if (this.started) {
throw new Error('Kernel has already been started in daemon');
}
Expand Down
3 changes: 2 additions & 1 deletion src/client/datascience/kernel-launcher/kernelLauncher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export class KernelLauncher implements IKernelLauncher {
public async launch(
kernelSpec: IJupyterKernelSpec,
resource: Resource,
workingDirectory: string,
interpreter?: PythonInterpreter
): Promise<IKernelProcess> {
const connection = await this.getKernelConnection();
Expand All @@ -47,7 +48,7 @@ export class KernelLauncher implements IKernelLauncher {
resource,
interpreter
);
await kernelProcess.launch();
await kernelProcess.launch(workingDirectory);
return kernelProcess;
}

Expand Down
Loading