Move SQL 2019 extension's notebook code into Azure Data Studio (#4090)

This commit is contained in:
Cory Rivera
2019-02-20 10:55:49 -08:00
committed by GitHub
parent 2dd71cbe26
commit 70838c3e24
66 changed files with 8098 additions and 14 deletions

View File

@@ -0,0 +1,82 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as vscode from 'vscode';
import * as sqlops from 'sqlops';
/**
* Wrapper class to act as a facade over VSCode and Data APIs and allow us to test / mock callbacks into
* this API from our code
*
* @export
* @class ApiWrapper
*/
export class ApiWrapper {
public createOutputChannel(name: string): vscode.OutputChannel {
return vscode.window.createOutputChannel(name);
}
public createTerminalWithOptions(options: vscode.TerminalOptions): vscode.Terminal {
return vscode.window.createTerminal(options);
}
public getCurrentConnection(): Thenable<sqlops.connection.Connection> {
return sqlops.connection.getCurrentConnection();
}
public getWorkspacePathFromUri(uri: vscode.Uri): string | undefined {
let workspaceFolder = vscode.workspace.getWorkspaceFolder(uri);
return workspaceFolder ? workspaceFolder.uri.fsPath : undefined;
}
public registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): vscode.Disposable {
return vscode.commands.registerCommand(command, callback, thisArg);
}
public registerCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, ...triggerCharacters: string[]): vscode.Disposable {
return vscode.languages.registerCompletionItemProvider(selector, provider, ...triggerCharacters);
}
public registerTaskHandler(taskId: string, handler: (profile: sqlops.IConnectionProfile) => void): void {
sqlops.tasks.registerTask(taskId, handler);
}
public showErrorMessage(message: string, ...items: string[]): Thenable<string | undefined> {
return vscode.window.showErrorMessage(message, ...items);
}
public showOpenDialog(options: vscode.OpenDialogOptions): Thenable<vscode.Uri[] | undefined> {
return vscode.window.showOpenDialog(options);
}
public startBackgroundOperation(operationInfo: sqlops.BackgroundOperationInfo): void {
sqlops.tasks.startBackgroundOperation(operationInfo);
}
/**
* Get the configuration for a extensionName
* @param extensionName The string name of the extension to get the configuration for
* @param resource The optional URI, as a URI object or a string, to use to get resource-scoped configurations
*/
public getConfiguration(extensionName?: string, resource?: vscode.Uri | string): vscode.WorkspaceConfiguration {
if (typeof resource === 'string') {
try {
resource = this.parseUri(resource);
} catch (e) {
resource = undefined;
}
} else if (!resource) {
// Fix to avoid adding lots of errors to debug console. Expects a valid resource or null, not undefined
resource = null;
}
return vscode.workspace.getConfiguration(extensionName, resource as vscode.Uri);
}
public parseUri(uri: string): vscode.Uri {
return vscode.Uri.parse(uri);
}
}

View File

@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as vscode from 'vscode';
import { ApiWrapper } from './apiWrapper';
/**
* Global context for the application
*/
export class AppContext {
private serviceMap: Map<string, any> = new Map();
constructor(public readonly extensionContext: vscode.ExtensionContext, public readonly apiWrapper: ApiWrapper) {
this.apiWrapper = apiWrapper || new ApiWrapper();
}
public getService<T>(serviceName: string): T {
return this.serviceMap.get(serviceName) as T;
}
public registerService<T>(serviceName: string, service: T): void {
this.serviceMap.set(serviceName, service);
}
}

View File

@@ -0,0 +1,48 @@
'use strict';
// CONFIG VALUES ///////////////////////////////////////////////////////////
export const extensionConfigSectionName = 'dataManagement';
export const extensionOutputChannel = 'SQL Server 2019 Preview';
export const configLogDebugInfo = 'logDebugInfo';
// JUPYTER CONFIG //////////////////////////////////////////////////////////
export const pythonBundleVersion = '0.0.1';
export const pythonVersion = '3.6.6';
export const sparkMagicVersion = '0.12.6.1';
export const python3 = 'python3';
export const pysparkkernel = 'pysparkkernel';
export const sparkkernel = 'sparkkernel';
export const pyspark3kernel = 'pyspark3kernel';
export const python3DisplayName = 'Python 3';
export const defaultSparkKernel = 'pyspark3kernel';
export const pythonPathConfigKey = 'pythonPath';
export const notebookConfigKey = 'notebook';
export const outputChannelName = 'dataManagement';
export const hdfsHost = 'host';
export const hdfsUser = 'user';
export const winPlatform = 'win32';
export const jupyterNotebookProviderId = 'jupyter';
export const jupyterConfigRootFolder = 'jupyter_config';
export const jupyterKernelsMasterFolder = 'kernels_master';
export const jupyterNotebookLanguageId = 'jupyter-notebook';
export const jupyterNotebookViewType = 'jupyter-notebook';
export const jupyterNewNotebookTask = 'jupyter.task.newNotebook';
export const jupyterOpenNotebookTask = 'jupyter.task.openNotebook';
export const jupyterNewNotebookCommand = 'jupyter.cmd.newNotebook';
export const jupyterCommandSetContext = 'jupyter.setContext';
export const jupyterCommandSetKernel = 'jupyter.setKernel';
export const jupyterReinstallDependenciesCommand = 'jupyter.reinstallDependencies';
export const jupyterAnalyzeCommand = 'jupyter.cmd.analyzeNotebook';
export const jupyterInstallPackages = 'jupyter.cmd.installPackages';
export const jupyterConfigurePython = 'jupyter.cmd.configurePython';
export enum BuiltInCommands {
SetContext = 'setContext'
}
export enum CommandContext {
WizardServiceEnabled = 'wizardservice:enabled'
}

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
// General Constants ///////////////////////////////////////////////////////
export const msgYes = localize('msgYes', 'Yes');
export const msgNo = localize('msgNo', 'No');
// Jupyter Constants ///////////////////////////////////////////////////////
export const msgManagePackagesPowershell = localize('msgManagePackagesPowershell', '<#\n--------------------------------------------------------------------------------\n\tThis is the sandboxed instance of python used by Jupyter server.\n\tTo install packages used by the python kernel use \'.\\python.exe -m pip install\'\n--------------------------------------------------------------------------------\n#>');
export const msgManagePackagesBash = localize('msgJupyterManagePackagesBash', ': \'\n--------------------------------------------------------------------------------\n\tThis is the sandboxed instance of python used by Jupyter server.\n\tTo install packages used by the python kernel use \'./python3.6 -m pip install\'\n--------------------------------------------------------------------------------\n\'');
export const msgManagePackagesCmd = localize('msgJupyterManagePackagesCmd', 'REM This is the sandboxed instance of python used by Jupyter server. To install packages used by the python kernel use \'.\\python.exe -m pip install\'');
export const msgSampleCodeDataFrame = localize('msgSampleCodeDataFrame', 'This sample code loads the file into a data frame and shows the first 10 results.');

View File

@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as crypto from 'crypto';
/**
* Creates a random token per https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback.
* Defaults to 24 bytes, which creates a 48-char hex string
*/
export function getRandomToken(size: number = 24): Promise<string> {
return new Promise((resolve, reject) => {
crypto.randomBytes(size, (err, buffer) => {
if (err) {
reject(err);
}
let token = buffer.toString('hex');
resolve(token);
});
});
}

View File

@@ -0,0 +1,115 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// This code is originally from https://github.com/Microsoft/vscode/blob/master/src/vs/base/node/ports.ts
'use strict';
import * as net from 'net';
export class StrictPortFindOptions {
constructor(public startPort: number, public minPort: number, public maxport: number) {
}
public maxRetriesPerStartPort: number = 5;
public totalRetryLoops: number = 10;
public timeout: number = 5000;
}
/**
* Searches for a free port with additional retries and a function to search in a much larger range if initial
* attempt to find a port fails. By skipping to a random port after the first time failing, this should help
* reduce the likelihood that no free port can be found.
*/
export async function strictFindFreePort(options: StrictPortFindOptions): Promise<number> {
let totalRetries = options.totalRetryLoops;
let startPort = options.startPort;
let port = await findFreePort(startPort, options.maxRetriesPerStartPort, options.timeout);
while (port === 0 && totalRetries > 0) {
startPort = getRandomInt(options.minPort, options.maxport);
port = await findFreePort(startPort, options.maxRetriesPerStartPort, options.timeout);
totalRetries--;
}
return port;
}
/**
* Get a random integer between `min` and `max`.
*
* @param {number} min - min number
* @param {number} max - max number
* @return {number} a random integer
*/
function getRandomInt(min, max): number {
return Math.floor(Math.random() * (max - min + 1) + min);
}
/**
* Given a start point and a max number of retries, will find a port that
* is openable. Will return 0 in case no free port can be found.
*/
export function findFreePort(startPort: number, giveUpAfter: number, timeout: number): Thenable<number> {
let done = false;
return new Promise(resolve => {
const timeoutHandle = setTimeout(() => {
if (!done) {
done = true;
return resolve(0);
}
}, timeout);
doFindFreePort(startPort, giveUpAfter, (port) => {
if (!done) {
done = true;
clearTimeout(timeoutHandle);
return resolve(port);
}
});
});
}
function doFindFreePort(startPort: number, giveUpAfter: number, clb: (port: number) => void): void {
if (giveUpAfter === 0) {
return clb(0);
}
const client = new net.Socket();
// If we can connect to the port it means the port is already taken so we continue searching
client.once('connect', () => {
dispose(client);
return doFindFreePort(startPort + 1, giveUpAfter - 1, clb);
});
client.once('data', () => {
// this listener is required since node.js 8.x
});
client.once('error', (err: Error & { code?: string }) => {
dispose(client);
// If we receive any non ECONNREFUSED error, it means the port is used but we cannot connect
if (err.code !== 'ECONNREFUSED') {
return doFindFreePort(startPort + 1, giveUpAfter - 1, clb);
}
// Otherwise it means the port is free to use!
return clb(startPort);
});
client.connect(startPort, '127.0.0.1');
}
function dispose(socket: net.Socket): void {
try {
socket.removeAllListeners('connect');
socket.removeAllListeners('error');
socket.end();
socket.destroy();
socket.unref();
} catch (error) {
console.error(error); // otherwise this error would get lost in the callback chain
}
}

View File

@@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
/**
* Deferred promise
*/
export class Deferred<T> {
promise: Promise<T>;
resolve: (value?: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
constructor() {
this.promise = new Promise<T>((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult>;
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => void): Thenable<TResult>;
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult> {
return this.promise.then(onfulfilled, onrejected);
}
}

View File

@@ -0,0 +1,126 @@
'use strict';
import * as childProcess from 'child_process';
import * as fs from 'fs-extra';
import * as nls from 'vscode-nls';
import * as vscode from 'vscode';
const localize = nls.loadMessageBundle();
export function getKnoxUrl(host: string, port: string): string {
return `https://${host}:${port}/gateway`;
}
export function getLivyUrl(serverName: string, port: string): string {
return this.getKnoxUrl(serverName, port) + '/default/livy/v1/';
}
export async function mkDir(dirPath: string, outputChannel?: vscode.OutputChannel): Promise<void> {
if (!await fs.exists(dirPath)) {
if (outputChannel) {
outputChannel.appendLine(localize('mkdirOutputMsg', '... Creating {0}', dirPath));
}
await fs.ensureDir(dirPath);
}
}
export function getErrorMessage(error: Error | string): string {
return (error instanceof Error) ? error.message : error;
}
// COMMAND EXECUTION HELPERS ///////////////////////////////////////////////
export function executeBufferedCommand(cmd: string, options: childProcess.ExecOptions, outputChannel?: vscode.OutputChannel): Thenable<string> {
return new Promise<string>((resolve, reject) => {
if (outputChannel) {
outputChannel.appendLine(` > ${cmd}`);
}
let child = childProcess.exec(cmd, options, (err, stdout) => {
if (err) {
reject(err);
} else {
resolve(stdout);
}
});
// Add listeners to print stdout and stderr if an output channel was provided
if (outputChannel) {
child.stdout.on('data', data => { outputDataChunk(data, outputChannel, ' stdout: '); });
child.stderr.on('data', data => { outputDataChunk(data, outputChannel, ' stderr: '); });
}
});
}
export function executeStreamedCommand(cmd: string, outputChannel?: vscode.OutputChannel): Thenable<void> {
return new Promise<void>((resolve, reject) => {
// Start the command
if (outputChannel) {
outputChannel.appendLine(` > ${cmd}`);
}
let child = childProcess.spawn(cmd, [], { shell: true, detached: false });
// Add listeners to resolve/reject the promise on exit
child.on('error', reject);
child.on('exit', (code: number) => {
if (code === 0) {
resolve();
} else {
reject(localize('executeCommandProcessExited', 'Process exited with code {0}', code));
}
});
// Add listeners to print stdout and stderr if an output channel was provided
if (outputChannel) {
child.stdout.on('data', data => { outputDataChunk(data, outputChannel, ' stdout: '); });
child.stderr.on('data', data => { outputDataChunk(data, outputChannel, ' stderr: '); });
}
});
}
export function getUserHome(): string {
return process.env.HOME || process.env.USERPROFILE;
}
export enum Platform {
Mac,
Linux,
Windows,
Others
}
export function getOSPlatform(): Platform {
switch (process.platform) {
case 'win32':
return Platform.Windows;
case 'darwin':
return Platform.Mac;
case 'linux':
return Platform.Linux;
default:
return Platform.Others;
}
}
export function getOSPlatformId(): string {
var platformId = undefined;
switch (process.platform) {
case 'win32':
platformId = 'win-x64';
break;
case 'darwin':
platformId = 'osx';
break;
default:
platformId = 'linux-x64';
break;
}
return platformId;
}
// PRIVATE HELPERS /////////////////////////////////////////////////////////
function outputDataChunk(data: string | Buffer, outputChannel: vscode.OutputChannel, header: string): void {
data.toString().split(/\r?\n/)
.forEach(line => {
outputChannel.appendLine(header + line);
});
}

View File

@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
export interface INotebook {
readonly cells: ICell[];
readonly metadata: INotebookMetadata;
readonly nbformat: number;
readonly nbformat_minor: number;
}
export interface INotebookMetadata {
kernelspec: IKernelInfo;
language_info?: ILanguageInfo;
}
export interface IKernelInfo {
name: string;
language?: string;
display_name?: string;
}
export interface ILanguageInfo {
name: string;
version: string;
mimetype?: string;
codemirror_mode?: string | ICodeMirrorMode;
}
export interface ICodeMirrorMode {
name: string;
version: string;
}
export interface ICell {
cell_type: CellType;
source: string | string[];
metadata: {
language?: string;
};
execution_count: number;
outputs?: ICellOutput[];
}
export type CellType = 'code' | 'markdown' | 'raw';
export class CellTypes {
public static readonly Code = 'code';
public static readonly Markdown = 'markdown';
public static readonly Raw = 'raw';
}
export interface ICellOutput {
output_type: OutputType;
}
export type OutputType =
| 'execute_result'
| 'display_data'
| 'stream'
| 'error'
| 'update_display_data';

View File

@@ -0,0 +1,171 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import * as sqlops from 'sqlops';
import * as fs from 'fs';
import * as utils from '../common/utils';
import { AppContext } from '../common/appContext';
import JupyterServerInstallation from '../jupyter/jupyterServerInstallation';
const localize = nls.loadMessageBundle();
export class ConfigurePythonDialog {
private dialog: sqlops.window.modelviewdialog.Dialog;
private readonly DialogTitle = localize('configurePython.dialogName', 'Configure Python for Notebooks');
private readonly OkButtonText = localize('configurePython.okButtonText', 'Install');
private readonly CancelButtonText = localize('configurePython.cancelButtonText', 'Cancel');
private readonly BrowseButtonText = localize('configurePython.browseButtonText', 'Change location');
private readonly LocationTextBoxTitle = localize('configurePython.locationTextBoxText', 'Notebook dependencies will be installed in this location');
private readonly SelectFileLabel = localize('configurePython.selectFileLabel', 'Select');
private readonly InstallationNote = localize('configurePython.installNote', 'This installation will take some time. It is recommended to not close the application until the installation is complete.');
private readonly InvalidLocationMsg = localize('configurePython.invalidLocationMsg', 'The specified install location is invalid.');
private pythonLocationTextBox: sqlops.InputBoxComponent;
private browseButton: sqlops.ButtonComponent;
constructor(private appContext: AppContext, private outputChannel: vscode.OutputChannel, private jupyterInstallation: JupyterServerInstallation) {
}
public async showDialog() {
this.dialog = sqlops.window.modelviewdialog.createDialog(this.DialogTitle);
this.initializeContent();
this.dialog.okButton.label = this.OkButtonText;
this.dialog.cancelButton.label = this.CancelButtonText;
this.dialog.registerCloseValidator(() => this.handleInstall());
sqlops.window.modelviewdialog.openDialog(this.dialog);
}
private initializeContent() {
this.dialog.registerContent(async view => {
this.pythonLocationTextBox = view.modelBuilder.inputBox()
.withProperties<sqlops.InputBoxProperties>({
value: JupyterServerInstallation.getPythonInstallPath(this.appContext.apiWrapper),
width: '100%'
}).component();
this.browseButton = view.modelBuilder.button()
.withProperties<sqlops.ButtonProperties>({
label: this.BrowseButtonText,
width: '100px'
}).component();
this.browseButton.onDidClick(() => this.handleBrowse());
let installationNoteText = view.modelBuilder.text().withProperties({
value: this.InstallationNote
}).component();
let noteWrapper = view.modelBuilder.flexContainer().component();
noteWrapper.addItem(installationNoteText, {
flex: '1 1 auto',
CSSStyles: {
'margin-top': '60px',
'padding-left': '15px',
'padding-right': '15px',
'border': '1px solid'
}
});
let formModel = view.modelBuilder.formContainer()
.withFormItems([{
component: this.pythonLocationTextBox,
title: this.LocationTextBoxTitle
}, {
component: this.browseButton,
title: undefined
}, {
component: noteWrapper,
title: undefined
}]).component();
await view.initializeModel(formModel);
});
}
private async handleInstall(): Promise<boolean> {
let pythonLocation = this.pythonLocationTextBox.value;
if (!pythonLocation || pythonLocation.length === 0) {
this.showErrorMessage(this.InvalidLocationMsg);
return false;
}
try {
let isValid = await this.isFileValid(pythonLocation);
if (!isValid) {
return false;
}
} catch (err) {
this.appContext.apiWrapper.showErrorMessage(utils.getErrorMessage(err));
return false;
}
// Don't wait on installation, since there's currently no Cancel functionality
this.jupyterInstallation.startInstallProcess(pythonLocation).catch(err => {
this.appContext.apiWrapper.showErrorMessage(utils.getErrorMessage(err));
});
return true;
}
private isFileValid(pythonLocation: string): Promise<boolean> {
let self = this;
return new Promise<boolean>(function (resolve) {
fs.stat(pythonLocation, function (err, stats) {
if (err) {
// Ignore error if folder doesn't exist, since it will be
// created during installation
if (err.code !== 'ENOENT') {
self.showErrorMessage(err.message);
resolve(false);
}
}
else {
if (stats.isFile()) {
self.showErrorMessage(self.InvalidLocationMsg);
resolve(false);
}
}
resolve(true);
});
});
}
private async handleBrowse(): Promise<void> {
let options: vscode.OpenDialogOptions = {
defaultUri: vscode.Uri.file(utils.getUserHome()),
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
openLabel: this.SelectFileLabel
};
let fileUris: vscode.Uri[] = await this.appContext.apiWrapper.showOpenDialog(options);
if (fileUris && fileUris[0]) {
this.pythonLocationTextBox.value = fileUris[0].fsPath;
}
}
private showInfoMessage(message: string) {
this.dialog.message = {
text: message,
level: sqlops.window.modelviewdialog.MessageLevel.Information
};
}
private showErrorMessage(message: string) {
this.dialog.message = {
text: message,
level: sqlops.window.modelviewdialog.MessageLevel.Error
};
}
}

View File

@@ -9,6 +9,11 @@ import * as vscode from 'vscode';
import * as sqlops from 'sqlops';
import * as os from 'os';
import * as nls from 'vscode-nls';
import { JupyterController } from './jupyter/jupyterController';
import { AppContext } from './common/appContext';
import { ApiWrapper } from './common/apiWrapper';
const localize = nls.loadMessageBundle();
const JUPYTER_NOTEBOOK_PROVIDER = 'jupyter';
@@ -17,6 +22,8 @@ const noNotebookVisible = localize('noNotebookVisible', 'No notebook editor is a
let counter = 0;
export let controller: JupyterController;
export function activate(extensionContext: vscode.ExtensionContext) {
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.new', (connectionId?: string) => {
newNotebook(connectionId);
@@ -37,6 +44,9 @@ export function activate(extensionContext: vscode.ExtensionContext) {
analyzeNotebook(explorerContext);
}));
let appContext = new AppContext(extensionContext, new ApiWrapper());
controller = new JupyterController(appContext);
controller.activate();
}
function newNotebook(connectionId: string) {
@@ -141,4 +151,7 @@ async function analyzeNotebook(oeContext?: sqlops.ObjectExplorerContext): Promis
// this method is called when your extension is deactivated
export function deactivate() {
if (controller) {
controller.deactivate();
}
}

View File

@@ -0,0 +1,132 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as should from 'should';
import * as assert from 'assert';
import * as vscode from 'vscode';
import * as sqlops from 'sqlops';
import * as tempWrite from 'temp-write';
import 'mocha';
import { JupyterController } from '../jupyter/jupyterController';
import { INotebook, CellTypes } from '../contracts/content';
describe('Notebook Integration Test', function (): void {
this.timeout(600000);
let expectedNotebookContent: INotebook = {
cells: [{
cell_type: CellTypes.Code,
source: '1+1',
metadata: { language: 'python' },
execution_count: 1
}],
metadata: {
'kernelspec': {
'name': 'pyspark3kernel',
'display_name': 'PySpark3'
}
},
nbformat: 4,
nbformat_minor: 2
};
it('Should connect to local notebook server with result 2', async function () {
this.timeout(60000);
let pythonNotebook = Object.assign({}, expectedNotebookContent, { metadata: { kernelspec: { name: 'python3', display_name: 'Python 3' } } });
let uri = writeNotebookToFile(pythonNotebook);
await ensureJupyterInstalled();
let notebook = await sqlops.nb.showNotebookDocument(uri);
should(notebook.document.cells).have.length(1);
let ran = await notebook.runCell(notebook.document.cells[0]);
should(ran).be.true('Notebook runCell failed');
let cellOutputs = notebook.document.cells[0].contents.outputs;
should(cellOutputs).have.length(1);
let result = (<sqlops.nb.IExecuteResult>cellOutputs[0]).data['text/plain'];
should(result).equal('2');
try {
// TODO support closing the editor. Right now this prompts and there's no override for this. Need to fix in core
// Close the editor using the recommended vscode API
//await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
}
catch (e) { }
});
it('Should connect to remote spark server with result 2', async function () {
this.timeout(240000);
let uri = writeNotebookToFile(expectedNotebookContent);
await ensureJupyterInstalled();
// Given a connection to a server exists
let connectionId = await connectToSparkIntegrationServer();
// When I open a Spark notebook and run the cell
let notebook = await sqlops.nb.showNotebookDocument(uri, {
connectionId: connectionId
});
should(notebook.document.cells).have.length(1);
let ran = await notebook.runCell(notebook.document.cells[0]);
should(ran).be.true('Notebook runCell failed');
// Then I expect to get the output result of 1+1, executed remotely against the Spark endpoint
let cellOutputs = notebook.document.cells[0].contents.outputs;
should(cellOutputs).have.length(4);
let sparkResult = (<sqlops.nb.IStreamResult>cellOutputs[3]).text;
should(sparkResult).equal('2');
try {
// TODO support closing the editor. Right now this prompts and there's no override for this. Need to fix in core
// Close the editor using the recommended vscode API
//await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
}
catch (e) { }
});
});
async function connectToSparkIntegrationServer(): Promise<string> {
assert.ok(process.env.BACKEND_HOSTNAME, 'BACKEND_HOSTNAME, BACKEND_USERNAME, BACKEND_PWD must be set using ./tasks/setbackenvariables.sh or .\\tasks\\setbackendvaraibles.bat');
let connInfo: sqlops.connection.Connection = {
options: {
'host': process.env.BACKEND_HOSTNAME,
'groupId': 'C777F06B-202E-4480-B475-FA416154D458',
'knoxport': '',
'user': process.env.BACKEND_USERNAME,
'password': process.env.BACKEND_PWD
},
providerName: 'HADOOP_KNOX',
connectionId: 'abcd1234',
};
connInfo['savePassword'] = true;
let result = await sqlops.connection.connect(<any>connInfo as sqlops.IConnectionProfile);
should(result.connected).be.true();
should(result.connectionId).not.be.undefined();
should(result.connectionId).not.be.empty();
should(result.errorMessage).be.undefined();
let activeConnections = await sqlops.connection.getActiveConnections();
should(activeConnections).have.length(1);
return result.connectionId;
}
function writeNotebookToFile(pythonNotebook: INotebook): vscode.Uri {
let notebookContentString = JSON.stringify(pythonNotebook);
let localFile = tempWrite.sync(notebookContentString, 'notebook.ipynb');
let uri = vscode.Uri.file(localFile);
return uri;
}
async function ensureJupyterInstalled(): Promise<void> {
let jupterControllerExports = vscode.extensions.getExtension('Microsoft.sql-vnext').exports;
let jupyterController = jupterControllerExports.getJupterController() as JupyterController;
await jupyterController.jupyterInstallation;
}

View File

@@ -0,0 +1,200 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { nb } from 'sqlops';
import * as vscode from 'vscode';
import { charCountToJsCountDiff, jsIndexToCharIndex } from './text';
import { JupyterNotebookProvider } from '../jupyter/jupyterNotebookProvider';
import { JupyterSessionManager } from '../jupyter/jupyterSessionManager';
import { Deferred } from '../common/promise';
const timeoutMilliseconds = 4000;
export class NotebookCompletionItemProvider implements vscode.CompletionItemProvider {
private _allDocuments: nb.NotebookDocument[];
private kernelDeferred = new Deferred<nb.IKernel>();
constructor(private _notebookProvider: JupyterNotebookProvider) {
}
public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext)
: vscode.ProviderResult<vscode.CompletionItem[] | vscode.CompletionList> {
this._allDocuments = nb.notebookDocuments;
let info = this.findMatchingCell(document);
this.isNotConnected(document, info);
// Get completions, with cancellation on timeout or if cancel is requested.
// Note that it's important we always return some value, or intellisense will never complete
let promises = [this.requestCompletions(info, position, document), this.onCanceled(token), this.onTimeout(timeoutMilliseconds)];
return Promise.race(promises);
}
public resolveCompletionItem(item: vscode.CompletionItem, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CompletionItem> {
return item;
}
private isNotConnected(document: vscode.TextDocument, info: INewIntellisenseInfo): void {
if (!info || !this._notebookProvider) {
return;
}
let notebookManager: nb.NotebookManager = undefined;
let kernel: nb.IKernel = undefined;
try {
this._notebookProvider.getNotebookManager(document.uri).then(manager => {
notebookManager = manager;
if (notebookManager) {
let sessionManager: JupyterSessionManager = <JupyterSessionManager>(notebookManager.sessionManager);
let sessions = sessionManager.listRunning();
if (sessions && sessions.length > 0) {
let session = sessions.find(session => session.path === info.notebook.uri.path);
if (!session) {
return;
}
kernel = session.kernel;
}
}
this.kernelDeferred.resolve(kernel);
});
} catch {
// If an exception occurs, swallow it currently
return;
}
}
private findMatchingCell(document: vscode.TextDocument): INewIntellisenseInfo {
if (this._allDocuments && document) {
for (let doc of this._allDocuments) {
for (let cell of doc.cells) {
if (cell && cell.uri && cell.uri.path === document.uri.path) {
return {
editorUri: cell.uri.path,
cell: cell,
notebook: doc
};
}
}
}
}
return undefined;
}
private async requestCompletions(info: INewIntellisenseInfo, position: vscode.Position, cellTextDocument: vscode.TextDocument): Promise<vscode.CompletionItem[]> {
let kernel = await this.kernelDeferred.promise;
this.kernelDeferred = new Deferred<nb.IKernel>();
if (!info || kernel === undefined || !kernel.supportsIntellisense || !kernel.isReady) {
return [];
}
let source = cellTextDocument.getText();
if (!source || source.length === 0) {
return [];
}
let cursorPosition = this.toCursorPosition(position, source);
let result = await kernel.requestComplete({
code: source,
cursor_pos: cursorPosition.adjustedPosition
});
if (!result || !result.content || result.content.status === 'error') {
return [];
}
let content = result.content;
// Get position relative to the current cursor.
let range = this.getEditRange(content, cursorPosition, position, source);
let items: vscode.CompletionItem[] = content.matches.map(m => {
let item: vscode.CompletionItem = {
label: m,
insertText: m,
kind: vscode.CompletionItemKind.Text,
textEdit: {
range: range,
newText: m,
newEol: undefined
}
};
return item;
});
return items;
}
private getEditRange(content: nb.ICompletionContent, cursorPosition: IRelativePosition, position: vscode.Position, source: string): vscode.Range {
let relativeStart = this.getRelativeStart(content, cursorPosition, source);
// For now we're not adjusting relativeEnd. This may be a subtle issue here: if this ever actually goes past the end character then we should probably
// account for the difference on the right-hand-side of the original text
let relativeEnd = content.cursor_end - cursorPosition.adjustedPosition;
let range = new vscode.Range(
new vscode.Position(position.line, Math.max(relativeStart + position.character, 0)),
new vscode.Position(position.line, Math.max(relativeEnd + position.character, 0)));
return range;
}
private getRelativeStart(content: nb.ICompletionContent, cursorPosition: IRelativePosition, source: string): number {
let relativeStart = 0;
if (content.cursor_start !== cursorPosition.adjustedPosition) {
// Account for possible surrogate characters inside the substring.
// We need to examine the substring between (start, end) for surrogates and add 1 char for each of these.
let diff = cursorPosition.adjustedPosition - content.cursor_start;
let startIndex = cursorPosition.originalPosition - diff;
let adjustedStart = content.cursor_start + charCountToJsCountDiff(source.slice(startIndex, cursorPosition.originalPosition));
relativeStart = adjustedStart - cursorPosition.adjustedPosition;
} else {
// It didn't change so leave at 0
relativeStart = 0;
}
return relativeStart;
}
private onCanceled(token: vscode.CancellationToken): Promise<vscode.CompletionItem[]> {
return new Promise((resolve, reject) => {
// On cancellation, quit
token.onCancellationRequested(() => resolve([]));
});
}
private onTimeout(timeout: number): Promise<vscode.CompletionItem[]> {
return new Promise((resolve, reject) => {
// After 4 seconds, quit
setTimeout(() => resolve([]), timeout);
});
}
/**
* Convert from a line+character position to a cursor position based on the whole string length
* Note: this is somewhat inefficient especially for large arrays. However we've done
* this for other intellisense libraries that are index based. The ideal would be to at
* least do caching of the contents in an efficient lookup structure so we don't have to recalculate
* and throw away each time.
*/
private toCursorPosition(position: vscode.Position, source: string): IRelativePosition {
let lines = source.split('\n');
let characterPosition = 0;
let currentLine = 0;
// Add up all lines up to the current one
for (currentLine; currentLine < position.line; currentLine++) {
// Add to the position, accounting for the \n at the end of the line
characterPosition += lines[currentLine].length + 1;
}
// Then add up to the cursor position on that line
characterPosition += position.character;
// Return the sum
return {
originalPosition: characterPosition,
adjustedPosition: jsIndexToCharIndex(characterPosition, source)
};
}
}
interface IRelativePosition {
originalPosition: number;
adjustedPosition: number;
}
export interface INewIntellisenseInfo {
editorUri: string;
cell: nb.NotebookCell;
notebook: nb.NotebookDocument;
}

View File

@@ -0,0 +1,72 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
// This code is originally from @jupyterlab/packages/coreutils/src/text.ts
// Note: this code doesn't seem to do anything in the sqlops environment since the
// surr
// javascript stores text as utf16 and string indices use "code units",
// which stores high-codepoint characters as "surrogate pairs",
// which occupy two indices in the javascript string.
// We need to translate cursor_pos in the Jupyter protocol (in characters)
// to js offset (with surrogate pairs taking two spots).
const HAS_SURROGATES: boolean = '𝐚'.length > 1;
/**
* Convert a javascript string index into a unicode character offset
*
* @param jsIdx - The javascript string index (counting surrogate pairs)
*
* @param text - The text in which the offset is calculated
*
* @returns The unicode character offset
*/
export function jsIndexToCharIndex(jsIdx: number, text: string): number {
if (!HAS_SURROGATES) {
// not using surrogates, nothing to do
return jsIdx;
}
let charIdx = jsIdx;
for (let i = 0; i + 1 < text.length && i < jsIdx; i++) {
let charCode = text.charCodeAt(i);
// check for surrogate pair
if (charCode >= 0xd800 && charCode <= 0xdbff) {
let nextCharCode = text.charCodeAt(i + 1);
if (nextCharCode >= 0xdc00 && nextCharCode <= 0xdfff) {
charIdx--;
i++;
}
}
}
return charIdx;
}
/**
* Get the diff between pure character count and JS-based count with 2 chars per surrogate pair.
*
* @param charIdx - The index in unicode characters
*
* @param text - The text in which the offset is calculated
*
* @returns The js-native index
*/
export function charCountToJsCountDiff(text: string): number {
let diff = 0;
if (!HAS_SURROGATES) {
// not using surrogates, nothing to do
return diff;
}
for (let i = 0; i + 1 < text.length; i++) {
let charCode = text.charCodeAt(i);
// check for surrogate pair
if (charCode >= 0xd800 && charCode <= 0xdbff) {
let nextCharCode = text.charCodeAt(i + 1);
if (nextCharCode >= 0xdc00 && nextCharCode <= 0xdfff) {
diff++;
i++;
}
}
}
return diff;
}

View File

@@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as vscode from 'vscode';
export interface IServerInstance {
readonly port: string;
readonly uri: vscode.Uri;
configure(): Promise<void>;
start(): Promise<void>;
stop(): Promise<void>;
}

View File

@@ -0,0 +1,260 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as path from 'path';
import * as sqlops from 'sqlops';
import * as vscode from 'vscode';
import * as os from 'os';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import * as constants from '../common/constants';
import * as localizedConstants from '../common/localizedConstants';
import JupyterServerInstallation from './jupyterServerInstallation';
import { IServerInstance } from './common';
import * as utils from '../common/utils';
import { IPrompter, QuestionTypes, IQuestion } from '../prompts/question';
import { AppContext } from '../common/appContext';
import { ApiWrapper } from '../common/apiWrapper';
import { LocalJupyterServerManager } from './jupyterServerManager';
import { NotebookCompletionItemProvider } from '../intellisense/completionItemProvider';
import { JupyterNotebookProvider } from './jupyterNotebookProvider';
import { ConfigurePythonDialog } from '../dialog/configurePythonDialog';
import CodeAdapter from '../prompts/adapter';
let untitledCounter = 0;
export class JupyterController implements vscode.Disposable {
private _jupyterInstallation: Promise<JupyterServerInstallation>;
private _notebookInstances: IServerInstance[] = [];
private outputChannel: vscode.OutputChannel;
private prompter: IPrompter;
constructor(private appContext: AppContext) {
this.prompter = new CodeAdapter();
this.outputChannel = this.appContext.apiWrapper.createOutputChannel(constants.extensionOutputChannel);
}
private get apiWrapper(): ApiWrapper {
return this.appContext.apiWrapper;
}
public get extensionContext(): vscode.ExtensionContext {
return this.appContext && this.appContext.extensionContext;
}
public dispose(): void {
this.deactivate();
}
// PUBLIC METHODS //////////////////////////////////////////////////////
public async activate(): Promise<boolean> {
// Prompt for install if the python installation path is not defined
let jupyterInstaller = new JupyterServerInstallation(
this.extensionContext.extensionPath,
this.outputChannel,
this.apiWrapper);
if (JupyterServerInstallation.isPythonInstalled(this.apiWrapper)) {
this._jupyterInstallation = Promise.resolve(jupyterInstaller);
} else {
this._jupyterInstallation = new Promise(resolve => {
jupyterInstaller.onInstallComplete(err => {
if (!err) {
resolve(jupyterInstaller);
}
});
});
}
let notebookProvider = undefined;
notebookProvider = this.registerNotebookProvider();
sqlops.nb.onDidOpenNotebookDocument(notebook => {
if (!JupyterServerInstallation.isPythonInstalled(this.apiWrapper)) {
this.doConfigurePython(jupyterInstaller);
}
});
// Add command/task handlers
this.apiWrapper.registerTaskHandler(constants.jupyterOpenNotebookTask, (profile: sqlops.IConnectionProfile) => {
return this.handleOpenNotebookTask(profile);
});
this.apiWrapper.registerTaskHandler(constants.jupyterNewNotebookTask, (profile: sqlops.IConnectionProfile) => {
return this.saveProfileAndCreateNotebook(profile);
});
this.apiWrapper.registerCommand(constants.jupyterNewNotebookCommand, (explorerContext: sqlops.ObjectExplorerContext) => {
return this.saveProfileAndCreateNotebook(explorerContext ? explorerContext.connectionProfile : undefined);
});
this.apiWrapper.registerCommand(constants.jupyterAnalyzeCommand, (explorerContext: sqlops.ObjectExplorerContext) => {
return this.saveProfileAndAnalyzeNotebook(explorerContext);
});
this.apiWrapper.registerCommand(constants.jupyterReinstallDependenciesCommand, () => { return this.handleDependenciesReinstallation(); });
this.apiWrapper.registerCommand(constants.jupyterInstallPackages, () => { return this.doManagePackages(); });
this.apiWrapper.registerCommand(constants.jupyterConfigurePython, () => { return this.doConfigurePython(jupyterInstaller); });
let supportedFileFilter: vscode.DocumentFilter[] = [
{ scheme: 'file', language: '*' },
{ scheme: 'untitled', language: '*' }
];
this.extensionContext.subscriptions.push(this.apiWrapper.registerCompletionItemProvider(supportedFileFilter, new NotebookCompletionItemProvider(notebookProvider)));
return true;
}
private registerNotebookProvider(): JupyterNotebookProvider {
let notebookProvider = new JupyterNotebookProvider((documentUri: vscode.Uri) => new LocalJupyterServerManager({
documentPath: documentUri.fsPath,
jupyterInstallation: this._jupyterInstallation,
extensionContext: this.extensionContext,
apiWrapper: this.apiWrapper
}));
sqlops.nb.registerNotebookProvider(notebookProvider);
return notebookProvider;
}
private saveProfileAndCreateNotebook(profile: sqlops.IConnectionProfile): Promise<void> {
return this.handleNewNotebookTask(undefined, profile);
}
private saveProfileAndAnalyzeNotebook(oeContext: sqlops.ObjectExplorerContext): Promise<void> {
return this.handleNewNotebookTask(oeContext, oeContext.connectionProfile);
}
public deactivate(): void {
// Shutdown any open notebooks
this._notebookInstances.forEach(instance => { instance.stop(); });
}
// EVENT HANDLERS //////////////////////////////////////////////////////
public async getDefaultConnection(): Promise<sqlops.ConnectionInfo> {
return await this.apiWrapper.getCurrentConnection();
}
private async handleOpenNotebookTask(profile: sqlops.IConnectionProfile): Promise<void> {
let notebookFileTypeName = localize('notebookFileType', 'Notebooks');
let filter = {};
filter[notebookFileTypeName] = 'ipynb';
let uris = await this.apiWrapper.showOpenDialog({
filters: filter,
canSelectFiles: true,
canSelectMany: false
});
if (uris && uris.length > 0) {
let fileUri = uris[0];
// Verify this is a .ipynb file since this isn't actually filtered on Mac/Linux
if (path.extname(fileUri.fsPath) !== '.ipynb') {
// in the future might want additional supported types
this.apiWrapper.showErrorMessage(localize('unsupportedFileType', 'Only .ipynb Notebooks are supported'));
} else {
await sqlops.nb.showNotebookDocument(fileUri, {
connectionId: profile.id,
providerId: constants.jupyterNotebookProviderId,
preview: false
});
}
}
}
private async handleNewNotebookTask(oeContext?: sqlops.ObjectExplorerContext, profile?: sqlops.IConnectionProfile): Promise<void> {
// Ensure we get a unique ID for the notebook. For now we're using a different prefix to the built-in untitled files
// to handle this. We should look into improving this in the future
let untitledUri = vscode.Uri.parse(`untitled:Notebook-${untitledCounter++}`);
let editor = await sqlops.nb.showNotebookDocument(untitledUri, {
connectionId: profile.id,
providerId: constants.jupyterNotebookProviderId,
preview: false,
defaultKernel: {
name: 'pyspark3kernel',
display_name: 'PySpark3',
language: 'python'
}
});
if (oeContext && oeContext.nodeInfo && oeContext.nodeInfo.nodePath) {
// Get the file path after '/HDFS'
let hdfsPath: string = oeContext.nodeInfo.nodePath.substring(oeContext.nodeInfo.nodePath.indexOf('/HDFS') + '/HDFS'.length);
if (hdfsPath.length > 0) {
let analyzeCommand = '#' + localizedConstants.msgSampleCodeDataFrame + os.EOL + 'df = (spark.read.option(\"inferSchema\", \"true\")'
+ os.EOL + '.option(\"header\", \"true\")' + os.EOL + '.csv(\'{0}\'))' + os.EOL + 'df.show(10)';
// TODO re-enable insert into document once APIs are finalized.
// editor.document.cells[0].source = [analyzeCommand.replace('{0}', hdfsPath)];
editor.edit(editBuilder => {
editBuilder.replace(0, {
cell_type: 'code',
source: analyzeCommand.replace('{0}', hdfsPath)
});
});
}
}
}
private async handleDependenciesReinstallation(): Promise<void> {
if (await this.confirmReinstall()) {
this._jupyterInstallation = JupyterServerInstallation.getInstallation(
this.extensionContext.extensionPath,
this.outputChannel,
this.apiWrapper,
undefined,
true);
}
}
//Confirmation message dialog
private async confirmReinstall(): Promise<boolean> {
return await this.prompter.promptSingle<boolean>(<IQuestion>{
type: QuestionTypes.confirm,
message: localize('confirmReinstall', 'Are you sure you want to reinstall?'),
default: true
});
}
public doManagePackages(): void {
try {
let terminal = this.apiWrapper.createTerminalWithOptions({ cwd: this.getPythonBinDir() });
terminal.show(true);
let shellType = this.apiWrapper.getConfiguration().get('terminal.integrated.shell.windows');
terminal.sendText(this.getTextToSendToTerminal(shellType), true);
} catch (error) {
let message = utils.getErrorMessage(error);
this.apiWrapper.showErrorMessage(message);
}
}
public async doConfigurePython(jupyterInstaller: JupyterServerInstallation): Promise<void> {
try {
let pythonDialog = new ConfigurePythonDialog(this.appContext, this.outputChannel, jupyterInstaller);
await pythonDialog.showDialog();
} catch (error) {
let message = utils.getErrorMessage(error);
this.apiWrapper.showErrorMessage(message);
}
}
public getTextToSendToTerminal(shellType: any): string {
if (utils.getOSPlatform() === utils.Platform.Windows && typeof shellType === 'string') {
if (shellType.endsWith('powershell.exe')) {
return localizedConstants.msgManagePackagesPowershell;
} else if (shellType.endsWith('cmd.exe')) {
return localizedConstants.msgManagePackagesCmd;
} else {
return localizedConstants.msgManagePackagesBash;
}
} else {
return localizedConstants.msgManagePackagesBash;
}
}
private getPythonBinDir(): string {
return JupyterServerInstallation.getPythonBinPath(this.apiWrapper);
}
public get jupyterInstallation() {
return this._jupyterInstallation;
}
}

View File

@@ -0,0 +1,172 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { nb } from 'sqlops';
import { Kernel, KernelMessage } from '@jupyterlab/services';
function toShellMessage(msgImpl: KernelMessage.IShellMessage): nb.IShellMessage {
return {
channel: msgImpl.channel,
type: msgImpl.channel,
content: msgImpl.content,
header: msgImpl.header,
parent_header: msgImpl.parent_header,
metadata: msgImpl.metadata
};
}
function toStdInMessage(msgImpl: KernelMessage.IStdinMessage): nb.IStdinMessage {
return {
channel: msgImpl.channel,
type: msgImpl.channel,
content: msgImpl.content,
header: msgImpl.header,
parent_header: msgImpl.parent_header,
metadata: msgImpl.metadata
};
}
function toIOPubMessage(msgImpl: KernelMessage.IIOPubMessage): nb.IIOPubMessage {
return {
channel: msgImpl.channel,
type: msgImpl.channel,
content: msgImpl.content,
header: msgImpl.header,
parent_header: msgImpl.parent_header,
metadata: msgImpl.metadata
};
}
function toIInputReply(content: nb.IInputReply): KernelMessage.IInputReply {
return {
value: content.value
};
}
export class JupyterKernel implements nb.IKernel {
constructor(private kernelImpl: Kernel.IKernelConnection) {
}
public get id(): string {
return this.kernelImpl.id;
}
public get name(): string {
return this.kernelImpl.name;
}
public get supportsIntellisense(): boolean {
return true;
}
public get isReady(): boolean {
return this.kernelImpl.isReady;
}
public get ready(): Promise<void> {
return this.kernelImpl.ready;
}
public get info(): nb.IInfoReply {
return this.kernelImpl.info as nb.IInfoReply;
}
public async getSpec(): Promise<nb.IKernelSpec> {
let specImpl = await this.kernelImpl.getSpec();
return {
name: specImpl.name,
display_name: specImpl.display_name
};
}
requestExecute(content: nb.IExecuteRequest, disposeOnDone?: boolean): nb.IFuture {
let futureImpl = this.kernelImpl.requestExecute(content as KernelMessage.IExecuteRequest, disposeOnDone);
return new JupyterFuture(futureImpl);
}
requestComplete(content: nb.ICompleteRequest): Promise<nb.ICompleteReplyMsg> {
return this.kernelImpl.requestComplete({
code: content.code,
cursor_pos: content.cursor_pos
}).then((completeMsg) => {
// Complete msg matches shell message definition, but with clearer content body
let msg: nb.ICompleteReplyMsg = toShellMessage(completeMsg);
return msg;
});
}
interrupt(): Promise<void> {
return this.kernelImpl.interrupt();
}
}
export class JupyterFuture implements nb.IFuture {
private _inProgress: boolean;
constructor(private futureImpl: Kernel.IFuture) {
this._inProgress = true;
}
public get msg(): nb.IShellMessage {
let msgImpl = this.futureImpl.msg;
return toShellMessage(msgImpl);
}
public get done(): Promise<nb.IShellMessage> {
// Convert on success, leave to throw original error on fail
return this.futureImpl.done.then((msgImpl) => {
this._inProgress = false;
return toShellMessage(msgImpl);
});
}
public get inProgress(): boolean {
return this._inProgress;
}
public set inProgress(inProg: boolean) {
this._inProgress = inProg;
}
setReplyHandler(handler: nb.MessageHandler<nb.IShellMessage>): void {
this.futureImpl.onReply = (msg) => {
let shellMsg = toShellMessage(msg);
return handler.handle(shellMsg);
};
}
setStdInHandler(handler: nb.MessageHandler<nb.IStdinMessage>): void {
this.futureImpl.onStdin = (msg) => {
let shellMsg = toStdInMessage(msg);
return handler.handle(shellMsg);
};
}
setIOPubHandler(handler: nb.MessageHandler<nb.IIOPubMessage>): void {
this.futureImpl.onIOPub = (msg) => {
let shellMsg = toIOPubMessage(msg);
return handler.handle(shellMsg);
};
}
registerMessageHook(hook: (msg: nb.IIOPubMessage) => boolean | PromiseLike<boolean>): void {
throw new Error('Method not implemented.');
}
removeMessageHook(hook: (msg: nb.IIOPubMessage) => boolean | PromiseLike<boolean>): void {
throw new Error('Method not implemented.');
}
sendInputReply(content: nb.IInputReply): void {
this.futureImpl.sendInputReply(toIInputReply(content));
}
dispose(): void {
this.futureImpl.dispose();
}
}

View File

@@ -0,0 +1,56 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { nb } from 'sqlops';
import * as vscode from 'vscode';
import { ServerConnection, SessionManager } from '@jupyterlab/services';
import { JupyterSessionManager } from './jupyterSessionManager';
import { ApiWrapper } from '../common/apiWrapper';
import { LocalJupyterServerManager } from './jupyterServerManager';
export class JupyterNotebookManager implements nb.NotebookManager, vscode.Disposable {
protected _serverSettings: ServerConnection.ISettings;
private _sessionManager: JupyterSessionManager;
constructor(private _serverManager: LocalJupyterServerManager, sessionManager?: JupyterSessionManager, private apiWrapper: ApiWrapper = new ApiWrapper()) {
this._sessionManager = sessionManager || new JupyterSessionManager();
this._serverManager.onServerStarted(() => {
this.setServerSettings(this._serverManager.serverSettings);
});
}
public get contentManager(): nb.ContentManager {
return undefined;
}
public get sessionManager(): nb.SessionManager {
return this._sessionManager;
}
public get serverManager(): nb.ServerManager {
return this._serverManager;
}
public get serverSettings(): ServerConnection.ISettings {
return this._serverSettings;
}
public setServerSettings(settings: Partial<ServerConnection.ISettings>): void {
this._serverSettings = ServerConnection.makeSettings(settings);
this._sessionManager.setJupyterSessionManager(new SessionManager({ serverSettings: this._serverSettings }));
}
dispose() {
if (this._sessionManager) {
this._sessionManager.shutdownAll().then(() => this._sessionManager.dispose());
}
if (this._serverManager) {
this._serverManager.stopServer().then(success => undefined, error => this.apiWrapper.showErrorMessage(error));
}
}
}

View File

@@ -0,0 +1,82 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { nb } from 'sqlops';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import * as constants from '../common/constants';
import { JupyterNotebookManager } from './jupyterNotebookManager';
import { LocalJupyterServerManager } from './jupyterServerManager';
export type ServerManagerFactory = (documentUri: vscode.Uri) => LocalJupyterServerManager;
export class JupyterNotebookProvider implements nb.NotebookProvider {
readonly providerId: string = constants.jupyterNotebookProviderId;
private managerTracker = new Map<string, JupyterNotebookManager>();
constructor(private createServerManager: ServerManagerFactory) {
}
public getNotebookManager(notebookUri: vscode.Uri): Thenable<nb.NotebookManager> {
if (!notebookUri) {
return Promise.reject(localize('errNotebookUriMissing', 'A notebook path is required'));
}
return this.doGetNotebookManager(notebookUri);
}
private doGetNotebookManager(notebookUri: vscode.Uri): Promise<nb.NotebookManager> {
let uriString = notebookUri.toString();
let manager = this.managerTracker.get(uriString);
if (!manager) {
let serverManager = this.createServerManager(notebookUri);
manager = new JupyterNotebookManager(serverManager);
this.managerTracker.set(uriString, manager);
}
return Promise.resolve(manager);
}
handleNotebookClosed(notebookUri: vscode.Uri): void {
if (!notebookUri) {
// As this is a notification method, will skip throwing an error here
return;
}
let uriString = notebookUri.toString();
let manager = this.managerTracker.get(uriString);
if (manager) {
this.managerTracker.delete(uriString);
manager.dispose();
}
}
public get standardKernels(): nb.IStandardKernel[] {
return [
{
"name": "Python 3",
"connectionProviderIds": []
},
{
"name": "PySpark",
"connectionProviderIds": ["HADOOP_KNOX"]
},
{
"name": "PySpark3",
"connectionProviderIds": ["HADOOP_KNOX"]
},
{
"name": "Spark | R",
"connectionProviderIds": ["HADOOP_KNOX"]
},
{
"name": "Spark | Scala",
"connectionProviderIds": ["HADOOP_KNOX"]
}
];
}
}

View File

@@ -0,0 +1,338 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as fs from 'fs-extra';
import * as path from 'path';
import * as nls from 'vscode-nls';
import * as sqlops from 'sqlops';
import { ExecOptions } from 'child_process';
import * as decompress from 'decompress';
import * as request from 'request';
import { ApiWrapper } from '../common/apiWrapper';
import * as constants from '../common/constants';
import * as utils from '../common/utils';
import { OutputChannel, ConfigurationTarget, Event, EventEmitter, window } from 'vscode';
const localize = nls.loadMessageBundle();
const msgPythonInstallationProgress = localize('msgPythonInstallationProgress', 'Python installation is in progress');
const msgPythonInstallationComplete = localize('msgPythonInstallationComplete', 'Python installation is complete');
const msgPythonDownloadError = localize('msgPythonDownloadError', 'Error while downloading python setup');
const msgPythonDownloadPending = localize('msgPythonDownloadPending', 'Downloading python package');
const msgPythonUnpackPending = localize('msgPythonUnpackPending', 'Unpacking python package');
const msgPythonDirectoryError = localize('msgPythonDirectoryError', 'Error while creating python installation directory');
const msgPythonUnpackError = localize('msgPythonUnpackError', 'Error while unpacking python bundle');
const msgTaskName = localize('msgTaskName', 'Installing Notebook dependencies');
const msgInstallPkgStart = localize('msgInstallPkgStart', 'Installing Notebook dependencies, see Tasks view for more information');
const msgInstallPkgFinish = localize('msgInstallPkgFinish', 'Notebook dependencies installation is complete');
function msgDependenciesInstallationFailed(errorMessage: string): string { return localize('msgDependenciesInstallationFailed', 'Installing Notebook dependencies failed with error: {0}', errorMessage); }
function msgDownloadPython(platform: string, pythonDownloadUrl: string): string { return localize('msgDownloadPython', 'Downloading local python for platform: {0} to {1}', platform, pythonDownloadUrl); }
export default class JupyterServerInstallation {
/**
* Path to the folder where all configuration sets will be stored. Should always be:
* %extension_path%/jupyter_config
*/
public apiWrapper: ApiWrapper;
public extensionPath: string;
public pythonBinPath: string;
public outputChannel: OutputChannel;
public configRoot: string;
public pythonEnvVarPath: string;
public execOptions: ExecOptions;
private _pythonInstallationPath: string;
private _pythonExecutable: string;
// Allows dependencies to be installed even if an existing installation is already present
private _forceInstall: boolean;
private static readonly DefaultPythonLocation = path.join(utils.getUserHome(), 'azuredatastudio-python');
private _installCompleteEmitter = new EventEmitter<string>();
constructor(extensionPath: string, outputChannel: OutputChannel, apiWrapper: ApiWrapper, pythonInstallationPath?: string, forceInstall?: boolean) {
this.extensionPath = extensionPath;
this.outputChannel = outputChannel;
this.apiWrapper = apiWrapper;
this._pythonInstallationPath = pythonInstallationPath || JupyterServerInstallation.getPythonInstallPath(this.apiWrapper);
this.configRoot = path.join(this.extensionPath, constants.jupyterConfigRootFolder);
this._forceInstall = !!forceInstall;
this.configurePackagePaths();
}
public get onInstallComplete(): Event<string> {
return this._installCompleteEmitter.event;
}
public static async getInstallation(
extensionPath: string,
outputChannel: OutputChannel,
apiWrapper: ApiWrapper,
pythonInstallationPath?: string,
forceInstall?: boolean): Promise<JupyterServerInstallation> {
let installation = new JupyterServerInstallation(extensionPath, outputChannel, apiWrapper, pythonInstallationPath, forceInstall);
await installation.startInstallProcess();
return installation;
}
private async installDependencies(backgroundOperation: sqlops.BackgroundOperation): Promise<void> {
if (!fs.existsSync(this._pythonExecutable) || this._forceInstall) {
window.showInformationMessage(msgInstallPkgStart);
this.outputChannel.show(true);
this.outputChannel.appendLine(msgPythonInstallationProgress);
backgroundOperation.updateStatus(sqlops.TaskStatus.InProgress, msgPythonInstallationProgress);
await this.installPythonPackage(backgroundOperation);
backgroundOperation.updateStatus(sqlops.TaskStatus.InProgress, msgPythonInstallationComplete);
this.outputChannel.appendLine(msgPythonInstallationComplete);
// Install jupyter on Windows because local python is not bundled with jupyter unlike linux and MacOS.
await this.installJupyterProsePackage();
await this.installSparkMagic();
backgroundOperation.updateStatus(sqlops.TaskStatus.Succeeded, msgInstallPkgFinish);
window.showInformationMessage(msgInstallPkgFinish);
}
}
private installPythonPackage(backgroundOperation: sqlops.BackgroundOperation): Promise<void> {
let bundleVersion = constants.pythonBundleVersion;
let pythonVersion = constants.pythonVersion;
let packageName = 'python-#pythonversion-#platform-#bundleversion.#extension';
let platformId = utils.getOSPlatformId();
packageName = packageName.replace('#platform', platformId)
.replace('#pythonversion', pythonVersion)
.replace('#bundleversion', bundleVersion)
.replace('#extension', process.platform === constants.winPlatform ? 'zip' : 'tar.gz');
let pythonDownloadUrl = undefined;
switch (utils.getOSPlatform()) {
case utils.Platform.Windows:
pythonDownloadUrl = 'https://go.microsoft.com/fwlink/?linkid=2065977';
break;
case utils.Platform.Mac:
pythonDownloadUrl = 'https://go.microsoft.com/fwlink/?linkid=2065976';
break;
default:
// Default to linux
pythonDownloadUrl = 'https://go.microsoft.com/fwlink/?linkid=2065975';
break;
}
let pythonPackagePathLocal = this._pythonInstallationPath + '/' + packageName;
let self = undefined;
return new Promise((resolve, reject) => {
self = this;
backgroundOperation.updateStatus(sqlops.TaskStatus.InProgress, msgDownloadPython(platformId, pythonDownloadUrl));
fs.mkdirs(this._pythonInstallationPath, (err) => {
if (err) {
backgroundOperation.updateStatus(sqlops.TaskStatus.InProgress, msgPythonDirectoryError);
reject(err);
}
let totalMegaBytes: number = undefined;
let receivedBytes = 0;
let printThreshold = 0.1;
request.get(pythonDownloadUrl, { timeout: 20000 })
.on('error', (downloadError) => {
backgroundOperation.updateStatus(sqlops.TaskStatus.InProgress, msgPythonDownloadError);
reject(downloadError);
})
.on('response', (response) => {
if (response.statusCode !== 200) {
backgroundOperation.updateStatus(sqlops.TaskStatus.InProgress, msgPythonDownloadError);
reject(response.statusMessage);
}
let totalBytes = parseInt(response.headers['content-length']);
totalMegaBytes = totalBytes / (1024 * 1024);
this.outputChannel.appendLine(`${msgPythonDownloadPending} (0 / ${totalMegaBytes.toFixed(2)} MB)`);
})
.on('data', (data) => {
receivedBytes += data.length;
if (totalMegaBytes) {
let receivedMegaBytes = receivedBytes / (1024 * 1024);
let percentage = receivedMegaBytes / totalMegaBytes;
if (percentage >= printThreshold) {
this.outputChannel.appendLine(`${msgPythonDownloadPending} (${receivedMegaBytes.toFixed(2)} / ${totalMegaBytes.toFixed(2)} MB)`);
printThreshold += 0.1;
}
}
})
.pipe(fs.createWriteStream(pythonPackagePathLocal))
.on('close', () => {
//unpack python zip/tar file
this.outputChannel.appendLine(msgPythonUnpackPending);
let pythonSourcePath = path.join(this._pythonInstallationPath, constants.pythonBundleVersion);
if (fs.existsSync(pythonSourcePath)) {
try {
fs.removeSync(pythonSourcePath);
} catch (err) {
backgroundOperation.updateStatus(sqlops.TaskStatus.InProgress, msgPythonUnpackError);
reject(err);
}
}
decompress(pythonPackagePathLocal, self._pythonInstallationPath).then(files => {
//Delete zip/tar file
fs.unlink(pythonPackagePathLocal, (err) => {
if (err) {
backgroundOperation.updateStatus(sqlops.TaskStatus.InProgress, msgPythonUnpackError);
reject(err);
}
});
resolve();
}).catch(err => {
backgroundOperation.updateStatus(sqlops.TaskStatus.InProgress, msgPythonUnpackError);
reject(err);
});
})
.on('error', (downloadError) => {
backgroundOperation.updateStatus(sqlops.TaskStatus.InProgress, msgPythonDownloadError);
reject(downloadError);
});
});
});
}
private configurePackagePaths(): void {
//Python source path up to bundle version
let pythonSourcePath = path.join(this._pythonInstallationPath, constants.pythonBundleVersion);
// Update python paths and properties to reference user's local python.
let pythonBinPathSuffix = process.platform === constants.winPlatform ? '' : 'bin';
this._pythonExecutable = path.join(pythonSourcePath, process.platform === constants.winPlatform ? 'python.exe' : 'bin/python3');
this.pythonBinPath = path.join(pythonSourcePath, pythonBinPathSuffix);
// Store paths to python libraries required to run jupyter.
this.pythonEnvVarPath = process.env.Path;
let delimiter = path.delimiter;
if (process.platform === constants.winPlatform) {
let pythonScriptsPath = path.join(pythonSourcePath, 'Scripts');
this.pythonEnvVarPath = pythonScriptsPath + delimiter + this.pythonEnvVarPath;
}
this.pythonEnvVarPath = this.pythonBinPath + delimiter + this.pythonEnvVarPath;
// Store the executable options to run child processes with env var without interfering parent env var.
let env = Object.assign({}, process.env);
env['PATH'] = this.pythonEnvVarPath;
this.execOptions = {
env: env
};
}
public async startInstallProcess(pythonInstallationPath?: string): Promise<void> {
if (pythonInstallationPath) {
this._pythonInstallationPath = pythonInstallationPath;
this.configurePackagePaths();
}
let updateConfig = () => {
let notebookConfig = this.apiWrapper.getConfiguration(constants.notebookConfigKey);
notebookConfig.update(constants.pythonPathConfigKey, this._pythonInstallationPath, ConfigurationTarget.Global);
};
if (!fs.existsSync(this._pythonExecutable) || this._forceInstall) {
this.apiWrapper.startBackgroundOperation({
displayName: msgTaskName,
description: msgTaskName,
isCancelable: false,
operation: op => {
this.installDependencies(op)
.then(() => {
this._installCompleteEmitter.fire();
updateConfig();
})
.catch(err => {
let errorMsg = msgDependenciesInstallationFailed(err);
op.updateStatus(sqlops.TaskStatus.Failed, errorMsg);
this.apiWrapper.showErrorMessage(errorMsg);
this._installCompleteEmitter.fire(errorMsg);
});
}
});
} else {
// Python executable already exists, but the path setting wasn't defined,
// so update it here
this._installCompleteEmitter.fire();
updateConfig();
}
}
private async installJupyterProsePackage(): Promise<void> {
if (process.platform === constants.winPlatform) {
let installJupyterCommand = `${this._pythonExecutable} -m pip install pandas==0.22.0 jupyter prose-codeaccelerator==1.3.0 --extra-index-url https://prose-python-packages.azurewebsites.net --no-warn-script-location`;
this.outputChannel.show(true);
this.outputChannel.appendLine(localize('msgInstallStart', 'Installing required packages to run Notebooks...'));
await utils.executeStreamedCommand(installJupyterCommand, this.outputChannel);
this.outputChannel.appendLine(localize('msgJupyterInstallDone', '... Jupyter installation complete.'));
} else {
return Promise.resolve();
}
}
private async installSparkMagic(): Promise<void> {
if (process.platform === constants.winPlatform) {
let sparkMagicPath = path.join(this.extensionPath, 'wheels/sparkmagic-#sparkMagicVersion-py3-none-any.whl'.replace('#sparkMagicVersion', constants.sparkMagicVersion));
let installSparkMagic = `${this._pythonExecutable} -m pip install ${sparkMagicPath} --no-warn-script-location`;
this.outputChannel.show(true);
this.outputChannel.appendLine(localize('msgInstallingSpark', 'Installing SparkMagic...'));
await utils.executeStreamedCommand(installSparkMagic, this.outputChannel);
} else {
return Promise.resolve();
}
}
public get pythonExecutable(): string {
return this._pythonExecutable;
}
public static isPythonInstalled(apiWrapper: ApiWrapper): boolean {
// Don't use _pythonExecutable here, since it could be populated with a default value
let pathSetting = JupyterServerInstallation.getPythonPathSetting(apiWrapper);
if (!pathSetting) {
return false;
}
let pythonExe = path.join(
pathSetting,
constants.pythonBundleVersion,
process.platform === constants.winPlatform ? 'python.exe' : 'bin/python3');
return fs.existsSync(pythonExe);
}
public static getPythonInstallPath(apiWrapper: ApiWrapper): string {
let userPath = JupyterServerInstallation.getPythonPathSetting(apiWrapper);
return userPath ? userPath : JupyterServerInstallation.DefaultPythonLocation;
}
private static getPythonPathSetting(apiWrapper: ApiWrapper): string {
let path = undefined;
if (apiWrapper) {
let notebookConfig = apiWrapper.getConfiguration(constants.notebookConfigKey);
if (notebookConfig) {
let configPythonPath = notebookConfig[constants.pythonPathConfigKey];
if (configPythonPath && fs.existsSync(configPythonPath)) {
path = configPythonPath;
}
}
}
return path;
}
public static getPythonBinPath(apiWrapper: ApiWrapper): string {
let pythonBinPathSuffix = process.platform === constants.winPlatform ? '' : 'bin';
return path.join(
JupyterServerInstallation.getPythonInstallPath(apiWrapper),
constants.pythonBundleVersion,
pythonBinPathSuffix);
}
}

View File

@@ -0,0 +1,147 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { nb } from 'sqlops';
import * as vscode from 'vscode';
import * as path from 'path';
import { ServerConnection } from '@jupyterlab/services';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { ApiWrapper } from '../common/apiWrapper';
import JupyterServerInstallation from './jupyterServerInstallation';
import * as utils from '../common/utils';
import { IServerInstance } from './common';
import { PerNotebookServerInstance, IInstanceOptions } from './serverInstance';
export interface IServerManagerOptions {
documentPath: string;
jupyterInstallation: Promise<JupyterServerInstallation>;
extensionContext: vscode.ExtensionContext;
apiWrapper?: ApiWrapper;
factory?: ServerInstanceFactory;
}
export class LocalJupyterServerManager implements nb.ServerManager, vscode.Disposable {
private _serverSettings: Partial<ServerConnection.ISettings>;
private _onServerStarted = new vscode.EventEmitter<void>();
private _instanceOptions: IInstanceOptions;
private apiWrapper: ApiWrapper;
private jupyterServer: IServerInstance;
factory: ServerInstanceFactory;
constructor(private options: IServerManagerOptions) {
this.apiWrapper = options.apiWrapper || new ApiWrapper();
this.factory = options.factory || new ServerInstanceFactory();
}
public get serverSettings(): Partial<ServerConnection.ISettings> {
return this._serverSettings;
}
public get isStarted(): boolean {
return !!this.jupyterServer;
}
public get instanceOptions(): IInstanceOptions {
return this._instanceOptions;
}
public get onServerStarted(): vscode.Event<void> {
return this._onServerStarted.event;
}
public async startServer(): Promise<void> {
try {
this.jupyterServer = await this.doStartServer();
this.options.extensionContext.subscriptions.push(this);
let partialSettings = LocalJupyterServerManager.getLocalConnectionSettings(this.jupyterServer.uri);
this._serverSettings = partialSettings;
this._onServerStarted.fire();
} catch (error) {
this.apiWrapper.showErrorMessage(localize('startServerFailed', 'Starting local Notebook server failed with error {0}', utils.getErrorMessage(error)));
throw error;
}
}
public dispose(): void {
this.stopServer().catch(err => {
let msg = utils.getErrorMessage(err);
this.apiWrapper.showErrorMessage(localize('shutdownError', 'Shutdown of Notebook server failed: {0}', msg));
});
}
public async stopServer(): Promise<void> {
if (this.jupyterServer) {
await this.jupyterServer.stop();
}
}
public static getLocalConnectionSettings(uri: vscode.Uri): Partial<ServerConnection.ISettings> {
return {
baseUrl: `${uri.scheme}://${uri.authority}`,
token: LocalJupyterServerManager.getToken(uri.query)
};
}
private static getToken(query: string): string {
if (query) {
let parts = query.split('=');
if (parts && parts.length >= 2) {
return parts[1];
}
}
return '';
}
private get documentPath(): string {
return this.options.documentPath;
}
private async doStartServer(): Promise<IServerInstance> { // We can't find or create servers until the installation is complete
let installation = await this.options.jupyterInstallation;
// Calculate the path to use as the notebook-dir for Jupyter based on the path of the uri of the
// notebook to open. This will be the workspace folder if the notebook uri is inside a workspace
// folder. Otherwise, it will be the folder that the notebook is inside. Ultimately, this means
// a new notebook server will be started for each folder a notebook is opened from.
//
// eg, opening:
// /path1/nb1.ipynb
// /path2/nb2.ipynb
// /path2/nb3.ipynb
// ... will result in 2 notebook servers being started, one for /path1/ and one for /path2/
let notebookDir = this.apiWrapper.getWorkspacePathFromUri(vscode.Uri.file(this.documentPath));
notebookDir = notebookDir || path.dirname(this.documentPath);
// TODO handle notification of start/stop status
// notebookContext.updateLoadingMessage(localizedConstants.msgJupyterStarting);
// TODO refactor server instance so it doesn't need the kernel. Likely need to reimplement this
// for notebook version
let serverInstanceOptions: IInstanceOptions = {
documentPath: this.documentPath,
notebookDirectory: notebookDir,
install: installation
};
this._instanceOptions = serverInstanceOptions;
let server = this.factory.createInstance(serverInstanceOptions);
await server.configure();
await server.start();
return server;
}
}
export class ServerInstanceFactory {
createInstance(options: IInstanceOptions): IServerInstance {
return new PerNotebookServerInstance(options);
}
}

View File

@@ -0,0 +1,338 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { nb, ServerInfo, connection, IConnectionProfile } from 'sqlops';
import { Session, Kernel } from '@jupyterlab/services';
import * as fs from 'fs-extra';
import * as nls from 'vscode-nls';
import { Uri } from 'vscode';
import * as path from 'path';
import * as utils from '../common/utils';
const localize = nls.loadMessageBundle();
import { JupyterKernel } from './jupyterKernel';
import { Deferred } from '../common/promise';
const configBase = {
'kernel_python_credentials': {
'url': ''
},
'kernel_scala_credentials': {
'url': ''
},
'kernel_r_credentials': {
'url': ''
},
'ignore_ssl_errors': true,
'logging_config': {
'version': 1,
'formatters': {
'magicsFormatter': {
'format': '%(asctime)s\t%(levelname)s\t%(message)s',
'datefmt': ''
}
},
'handlers': {
'magicsHandler': {
'class': 'hdijupyterutils.filehandler.MagicsFileHandler',
'formatter': 'magicsFormatter',
'home_path': ''
}
},
'loggers': {
'magicsLogger': {
'handlers': ['magicsHandler'],
'level': 'DEBUG',
'propagate': 0
}
}
}
};
const KNOX_ENDPOINT_SERVER = 'host';
const KNOX_ENDPOINT_PORT = 'knoxport';
const KNOX_ENDPOINT = 'knox';
const SQL_PROVIDER = 'MSSQL';
const USER = 'user';
const DEFAULT_CLUSTER_USER_NAME = 'root';
export class JupyterSessionManager implements nb.SessionManager {
private _ready: Deferred<void>;
private _isReady: boolean;
private _sessionManager: Session.IManager;
private static _sessions: JupyterSession[] = [];
constructor() {
this._isReady = false;
this._ready = new Deferred<void>();
}
public setJupyterSessionManager(sessionManager: Session.IManager): void {
this._sessionManager = sessionManager;
sessionManager.ready
.then(() => {
this._isReady = true;
this._ready.resolve();
}).catch((error) => {
this._isReady = false;
this._ready.reject(error);
});
}
public get isReady(): boolean {
return this._isReady;
}
public get ready(): Promise<void> {
return this._ready.promise;
}
public get specs(): nb.IAllKernels | undefined {
if (!this._isReady) {
return undefined;
}
let specs = this._sessionManager.specs;
if (!specs) {
return undefined;
}
let kernels: nb.IKernelSpec[] = Object.keys(specs.kernelspecs).map(k => {
let value = specs.kernelspecs[k];
let kernel: nb.IKernelSpec = {
name: k,
display_name: value.display_name ? value.display_name : k
};
// TODO add more info to kernels
return kernel;
});
let allKernels: nb.IAllKernels = {
defaultKernel: specs.default,
kernels: kernels
};
return allKernels;
}
public async startNew(options: nb.ISessionOptions): Promise<nb.ISession> {
if (!this._isReady) {
// no-op
return Promise.reject(new Error(localize('errorStartBeforeReady', 'Cannot start a session, the manager is not yet initialized')));
}
let sessionImpl = await this._sessionManager.startNew(options);
let jupyterSession = new JupyterSession(sessionImpl);
let index = JupyterSessionManager._sessions.findIndex(session => session.path === options.path);
if (index > -1) {
JupyterSessionManager._sessions.splice(index);
}
JupyterSessionManager._sessions.push(jupyterSession);
return jupyterSession;
}
public listRunning(): JupyterSession[] {
return JupyterSessionManager._sessions;
}
public shutdown(id: string): Promise<void> {
if (!this._isReady) {
// no-op
return Promise.resolve();
}
let index = JupyterSessionManager._sessions.findIndex(session => session.id === id);
if (index > -1) {
JupyterSessionManager._sessions.splice(index);
}
if (this._sessionManager && !this._sessionManager.isDisposed) {
return this._sessionManager.shutdown(id);
}
}
public shutdownAll(): Promise<void> {
return this._sessionManager.shutdownAll();
}
public dispose(): void {
this._sessionManager.dispose();
}
}
export class JupyterSession implements nb.ISession {
private _kernel: nb.IKernel;
constructor(private sessionImpl: Session.ISession) {
}
public get canChangeKernels(): boolean {
return true;
}
public get id(): string {
return this.sessionImpl.id;
}
public get path(): string {
return this.sessionImpl.path;
}
public get name(): string {
return this.sessionImpl.name;
}
public get type(): string {
return this.sessionImpl.type;
}
public get status(): nb.KernelStatus {
return this.sessionImpl.status;
}
public get kernel(): nb.IKernel {
if (!this._kernel) {
let kernelImpl = this.sessionImpl.kernel;
if (kernelImpl) {
this._kernel = new JupyterKernel(kernelImpl);
}
}
return this._kernel;
}
public async changeKernel(kernelInfo: nb.IKernelSpec): Promise<nb.IKernel> {
// For now, Jupyter implementation handles disposal etc. so we can just
// null out our kernel and let the changeKernel call handle this
this._kernel = undefined;
// For now, just using name. It's unclear how we'd know the ID
let options: Partial<Kernel.IModel> = {
name: kernelInfo.name
};
return this.sessionImpl.changeKernel(options).then((kernelImpl) => {
this._kernel = new JupyterKernel(kernelImpl);
return this._kernel;
});
}
public async configureKernel(): Promise<void> {
let sparkmagicConfDir = path.join(utils.getUserHome(), '.sparkmagic');
await utils.mkDir(sparkmagicConfDir);
// Default to localhost in config file.
let creds: ICredentials = {
'url': 'http://localhost:8088'
};
let config: ISparkMagicConfig = Object.assign({}, configBase);
this.updateConfig(config, creds, sparkmagicConfDir);
let configFilePath = path.join(sparkmagicConfDir, 'config.json');
await fs.writeFile(configFilePath, JSON.stringify(config));
}
public async configureConnection(connection: IConnectionProfile): Promise<void> {
if (connection && connection.providerName && this.isSparkKernel(this.sessionImpl.kernel.name)) {
// TODO may need to reenable a way to get the credential
// await this._connection.getCredential();
// %_do_not_call_change_endpoint is a SparkMagic command that lets users change endpoint options,
// such as user/profile/host name/auth type
//Update server info with bigdata endpoint - Unified Connection
if (connection.providerName === SQL_PROVIDER) {
let clusterEndpoint: IEndpoint = await this.getClusterEndpoint(connection.id, KNOX_ENDPOINT);
if (!clusterEndpoint) {
let kernelDisplayName: string = await this.getKernelDisplayName();
return Promise.reject(new Error(localize('connectionNotValid', 'Spark kernels require a connection to a SQL Server big data cluster master instance.')));
}
connection.options[KNOX_ENDPOINT_SERVER] = clusterEndpoint.ipAddress;
connection.options[KNOX_ENDPOINT_PORT] = clusterEndpoint.port;
connection.options[USER] = DEFAULT_CLUSTER_USER_NAME;
}
else {
connection.options[KNOX_ENDPOINT_PORT] = this.getKnoxPortOrDefault(connection);
}
this.setHostAndPort(':', connection);
this.setHostAndPort(',', connection);
let server = Uri.parse(utils.getLivyUrl(connection.options[KNOX_ENDPOINT_SERVER], connection.options[KNOX_ENDPOINT_PORT])).toString();
let doNotCallChangeEndpointParams =
`%_do_not_call_change_endpoint --username=${connection.options[USER]} --password=${connection.options['password']} --server=${server} --auth=Basic_Access`;
let future = this.sessionImpl.kernel.requestExecute({
code: doNotCallChangeEndpointParams
}, true);
await future.done;
}
}
private async getKernelDisplayName(): Promise<string> {
let spec = await this.kernel.getSpec();
return spec.display_name;
}
private isSparkKernel(kernelName: string): boolean {
return kernelName && kernelName.toLowerCase().indexOf('spark') > -1;
}
private setHostAndPort(delimeter: string, connection: IConnectionProfile): void {
let originalHost = connection.options[KNOX_ENDPOINT_SERVER];
if (!originalHost) {
return;
}
let index = originalHost.indexOf(delimeter);
if (index > -1) {
connection.options[KNOX_ENDPOINT_SERVER] = originalHost.slice(0, index);
connection.options[KNOX_ENDPOINT_PORT] = originalHost.slice(index + 1);
}
}
private updateConfig(config: ISparkMagicConfig, creds: ICredentials, homePath: string): void {
config.kernel_python_credentials = creds;
config.kernel_scala_credentials = creds;
config.kernel_r_credentials = creds;
config.logging_config.handlers.magicsHandler.home_path = homePath;
}
private getKnoxPortOrDefault(connectionProfile: IConnectionProfile): string {
let port = connectionProfile.options[KNOX_ENDPOINT_PORT];
if (!port) {
port = '30443';
}
return port;
}
private async getClusterEndpoint(profileId: string, serviceName: string): Promise<IEndpoint> {
let serverInfo: ServerInfo = await connection.getServerInfo(profileId);
if (!serverInfo || !serverInfo.options) {
return undefined;
}
let endpoints: IEndpoint[] = serverInfo.options['clusterEndpoints'];
if (!endpoints || endpoints.length === 0) {
return undefined;
}
return endpoints.find(ep => ep.serviceName.toLowerCase() === serviceName.toLowerCase());
}
}
interface ICredentials {
'url': string;
}
interface IEndpoint {
serviceName: string;
ipAddress: string;
port: number;
}
interface ISparkMagicConfig {
kernel_python_credentials: ICredentials;
kernel_scala_credentials: ICredentials;
kernel_r_credentials: ICredentials;
ignore_ssl_errors?: boolean;
logging_config: {
handlers: {
magicsHandler: {
home_path: string;
class?: string;
formatter?: string
}
}
};
}

View File

@@ -0,0 +1,76 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as fs from 'fs-extra';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import * as constants from '../common/constants';
export enum SettingType {
String,
Number,
Boolean,
Set
}
export class ISetting {
key: string;
value: string | number | boolean;
type: SettingType;
}
export class JupyterSettingWriter {
private settings: ISetting[] = [];
constructor(private baseFile: string) {
}
public addSetting(setting: ISetting): void {
this.settings.push(setting);
}
public async writeSettings(targetFile: string): Promise<void> {
let settings = await this.printSettings();
await fs.writeFile(targetFile, settings);
}
public async printSettings(): Promise<string> {
let content = '';
let newLine = process.platform === constants.winPlatform ? '\r\n' : '\n';
if (this.baseFile) {
let sourceContents = await fs.readFile(this.baseFile);
content += sourceContents.toString();
}
for (let setting of this.settings) {
content += newLine;
content += this.printSetting(setting);
}
return content;
}
private printSetting(setting: ISetting): string {
let value: string;
switch (setting.type) {
case SettingType.Boolean:
value = setting.value ? 'True' : 'False';
break;
case SettingType.String:
value = `'${setting.value}'`;
break;
case SettingType.Number:
value = `${setting.value}`;
break;
case SettingType.Set:
value = `set([${setting.value}])`;
break;
default:
throw new Error(localize('UnexpectedSettingType', 'Unexpected setting type {0}', setting.type));
}
return `c.${setting.key} = ${value}`;
}
}

View File

@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { nb } from 'sqlops';
import * as vscode from 'vscode';
import { Contents } from '@jupyterlab/services';
export class RemoteContentManager implements nb.ContentManager {
constructor(private contents: Contents.IManager) {
}
public getNotebookContents(notebookUri: vscode.Uri): Thenable<nb.INotebookContents> {
return this.getNotebookContentsAsync(notebookUri.fsPath);
}
private async getNotebookContentsAsync(path: string): Promise<nb.INotebookContents> {
if (!path) {
return undefined;
}
// Note: intentionally letting caller handle exceptions
let contentsModel = await this.contents.get(path);
if (!contentsModel) {
return undefined;
}
return <nb.INotebookContents>contentsModel.content;
}
public async save(notebookUri: vscode.Uri, notebook: nb.INotebookContents): Promise<nb.INotebookContents> {
let path = notebookUri.fsPath;
await this.contents.save(path, {
path: path,
content: notebook,
type: 'notebook',
format: 'json'
});
return notebook;
}
}

View File

@@ -0,0 +1,399 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs-extra';
import * as os from 'os';
import { spawn, ExecOptions, SpawnOptions, ChildProcess } from 'child_process';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { IServerInstance } from './common';
import JupyterServerInstallation from './jupyterServerInstallation';
import * as utils from '../common/utils';
import * as constants from '../common/constants';
import * as notebookUtils from '../common/notebookUtils';
import * as ports from '../common/ports';
const NotebookConfigFilename = 'jupyter_notebook_config.py';
const CustomJsFilename = 'custom.js';
const defaultPort = 8888;
const JupyterStartedMessage = 'The Jupyter Notebook is running';
type MessageListener = (data: string | Buffer) => void;
type ErrorListener = (err: any) => void;
export interface IInstanceOptions {
/**
* The path to the initial document we want to start this server for
*/
documentPath: string;
/**
* Base install information needed in order to start the server instance
*/
install: JupyterServerInstallation;
/**
* Optional start directory for the notebook server. If none is set, will use a
* path relative to the initial document
*/
notebookDirectory?: string;
}
/**
* Helper class to enable testing without calling into file system or
* commandline shell APIs
*/
export class ServerInstanceUtils {
public mkDir(dirPath: string, outputChannel?: vscode.OutputChannel): Promise<void> {
return utils.mkDir(dirPath, outputChannel);
}
public removeDir(dirPath: string): Promise<void> {
return fs.remove(dirPath);
}
public pathExists(dirPath: string): Promise<boolean> {
return fs.pathExists(dirPath);
}
public copy(src: string, dest: string): Promise<void> {
return fs.copy(src, dest);
}
public existsSync(dirPath: string): boolean {
return fs.existsSync(dirPath);
}
public generateUuid(): string {
return UUID.generateUuid();
}
public executeBufferedCommand(cmd: string, options: ExecOptions, outputChannel?: vscode.OutputChannel): Thenable<string> {
return utils.executeBufferedCommand(cmd, options, outputChannel);
}
public spawn(command: string, args?: ReadonlyArray<string>, options?: SpawnOptions): ChildProcess {
return spawn(command, args, options);
}
public checkProcessDied(childProcess: ChildProcess): void {
if (!childProcess) {
return;
}
// Wait 10 seconds and then force kill. Jupyter stop is slow so this seems a reasonable time limit
setTimeout(() => {
// Test if the process is still alive. Throws an exception if not
try {
process.kill(childProcess.pid, <any>0);
} catch (error) {
// All is fine.
}
}, 10000);
}
}
export class PerNotebookServerInstance implements IServerInstance {
/**
* Root of the jupyter directory structure. Config and data roots will be
* under this, in order to simplify deletion of folders on stop of the instance
*/
private baseDir: string;
/**
* Path to configuration folder for this instance. Typically:
* %extension_path%/jupyter_config/%server%_config
*/
private instanceConfigRoot: string;
/**
* Path to data folder for this instance. Typically:
* %extension_path%/jupyter_config/%server%_data
*/
private instanceDataRoot: string;
private _systemJupyterDir: string;
private _port: string;
private _uri: vscode.Uri;
private _isStarted: boolean = false;
private utils: ServerInstanceUtils;
private childProcess: ChildProcess;
private errorHandler: ErrorHandler = new ErrorHandler();
constructor(private options: IInstanceOptions, fsUtils?: ServerInstanceUtils) {
this.utils = fsUtils || new ServerInstanceUtils();
}
public get isStarted(): boolean {
return this._isStarted;
}
public get port(): string {
return this._port;
}
public get uri(): vscode.Uri {
return this._uri;
}
public async configure(): Promise<void> {
await this.configureJupyter();
}
public async start(): Promise<void> {
await this.startInternal();
}
public async stop(): Promise<void> {
try {
if (this.baseDir) {
let exists = await this.utils.pathExists(this.baseDir);
if (exists) {
await this.utils.removeDir(this.baseDir);
}
}
if (this.isStarted) {
let install = this.options.install;
let stopCommand = `${install.pythonExecutable} -m jupyter notebook stop ${this._port}`;
await this.utils.executeBufferedCommand(stopCommand, install.execOptions, install.outputChannel);
this._isStarted = false;
this.utils.checkProcessDied(this.childProcess);
this.handleConnectionClosed();
}
} catch (error) {
// For now, we don't care as this is non-critical
this.notify(this.options.install, localize('serverStopError', 'Error stopping Notebook Server: {0}', utils.getErrorMessage(error)));
}
}
private async configureJupyter(): Promise<void> {
await this.createInstanceFolders();
let resourcesFolder = path.join(this.options.install.extensionPath, 'resources', constants.jupyterConfigRootFolder);
await this.copyInstanceConfig(resourcesFolder);
await this.CopyCustomJs(resourcesFolder);
await this.copyKernelsToSystemJupyterDirs();
}
private async createInstanceFolders(): Promise<void> {
this.baseDir = path.join(this.options.install.configRoot, 'instances', `${this.utils.generateUuid()}`);
this.instanceConfigRoot = path.join(this.baseDir, 'config');
this.instanceDataRoot = path.join(this.baseDir, 'data');
await this.utils.mkDir(this.baseDir, this.options.install.outputChannel);
await this.utils.mkDir(this.instanceConfigRoot, this.options.install.outputChannel);
await this.utils.mkDir(this.instanceDataRoot, this.options.install.outputChannel);
}
private async copyInstanceConfig(resourcesFolder: string): Promise<void> {
let configSource = path.join(resourcesFolder, NotebookConfigFilename);
let configDest = path.join(this.instanceConfigRoot, NotebookConfigFilename);
await this.utils.copy(configSource, configDest);
}
private async CopyCustomJs(resourcesFolder: string): Promise<void> {
let customPath = path.join(this.instanceConfigRoot, 'custom');
await this.utils.mkDir(customPath, this.options.install.outputChannel);
let customSource = path.join(resourcesFolder, CustomJsFilename);
let customDest = path.join(customPath, CustomJsFilename);
await this.utils.copy(customSource, customDest);
}
private async copyKernelsToSystemJupyterDirs(): Promise<void> {
let kernelsExtensionSource = path.join(this.options.install.extensionPath, 'kernels');
this._systemJupyterDir = this.getSystemJupyterKernelDir();
if (!this.utils.existsSync(this._systemJupyterDir)) {
await this.utils.mkDir(this._systemJupyterDir, this.options.install.outputChannel);
}
await this.utils.copy(kernelsExtensionSource, this._systemJupyterDir);
}
private getSystemJupyterKernelDir(): string {
switch (process.platform) {
case 'win32':
let appDataWindows = process.env['APPDATA'];
return appDataWindows + '\\jupyter\\kernels';
case 'darwin':
return path.resolve(os.homedir(), 'Library/Jupyter/kernels');
default:
return path.resolve(os.homedir(), '.local/share/jupyter/kernels');
}
}
/**
* Starts a Jupyter instance using the provided a start command. Server is determined to have
* started when the log message with URL to connect to is emitted.
* @returns {Promise<void>}
*/
protected async startInternal(): Promise<void> {
if (this.isStarted) {
return;
}
let notebookDirectory = this.getNotebookDirectory();
// Find a port in a given range. If run into trouble, got up 100 in range and search inside a larger range
let port = await ports.strictFindFreePort(new ports.StrictPortFindOptions(defaultPort, defaultPort + 100, defaultPort + 1000));
let token = await notebookUtils.getRandomToken();
this._uri = vscode.Uri.parse(`http://localhost:${port}/?token=${token}`);
this._port = port.toString();
let startCommand = `${this.options.install.pythonExecutable} -m jupyter notebook --no-browser --notebook-dir "${notebookDirectory}" --port=${port} --NotebookApp.token=${token}`;
this.notifyStarting(this.options.install, startCommand);
// Execute the command
await this.executeStartCommand(startCommand);
}
private executeStartCommand(startCommand: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
let install = this.options.install;
this.childProcess = this.spawnJupyterProcess(install, startCommand);
// Add listeners for the process exiting prematurely
let onErrorBeforeStartup = (err) => reject(err);
let onExitBeforeStart = (err) => {
if (!this.isStarted) {
reject(localize('notebookStartProcessExitPremature', 'Notebook process exited prematurely with error: {0}', err));
}
};
this.childProcess.on('error', onErrorBeforeStartup);
this.childProcess.on('exit', onExitBeforeStart);
// Add listener for the process to emit its web address
let handleStdout = (data: string | Buffer) => { install.outputChannel.appendLine(data.toString()); };
let handleStdErr = (data: string | Buffer) => {
// For some reason, URL info is sent on StdErr
let [url, port] = this.matchUrlAndPort(data);
if (url) {
// For now, will verify port matches
if (url.authority !== this._uri.authority
|| url.query !== this._uri.query) {
this._uri = url;
this._port = port;
}
this.notifyStarted(install, url.toString());
this._isStarted = true;
this.updateListeners(handleStdout, handleStdErr, onErrorBeforeStartup, onExitBeforeStart);
resolve();
}
};
this.childProcess.stdout.on('data', handleStdout);
this.childProcess.stderr.on('data', handleStdErr);
});
}
private updateListeners(handleStdout: MessageListener, handleStdErr: MessageListener, onErrorBeforeStartup: ErrorListener, onExitBeforeStart: ErrorListener): void {
this.childProcess.stdout.removeListener('data', handleStdout);
this.childProcess.stderr.removeListener('data', handleStdErr);
this.childProcess.removeListener('error', onErrorBeforeStartup);
this.childProcess.removeListener('exit', onExitBeforeStart);
this.childProcess.addListener('error', this.handleConnectionError);
this.childProcess.addListener('exit', this.handleConnectionClosed);
// TODO #897 covers serializing stdout and stderr to a location where we can read from so that user can see if they run into trouble
}
private handleConnectionError(error: Error): void {
let action = this.errorHandler.handleError(error);
if (action === ErrorAction.Shutdown) {
this.notify(this.options.install, localize('jupyterError', 'Error sent from Jupyter: {0}', utils.getErrorMessage(error)));
this.stop();
}
}
private handleConnectionClosed(): void {
this.childProcess = undefined;
this._isStarted = false;
}
getNotebookDirectory(): string {
if (this.options.notebookDirectory) {
if (this.options.notebookDirectory.endsWith('\\')) {
return this.options.notebookDirectory.substr(0, this.options.notebookDirectory.length - 1) + '/';
}
return this.options.notebookDirectory;
}
return path.dirname(this.options.documentPath);
}
private matchUrlAndPort(data: string | Buffer): [vscode.Uri, string] {
// regex: Looks for the successful startup log message like:
// [C 12:08:51.947 NotebookApp]
//
// Copy/paste this URL into your browser when you connect for the first time,
// to login with a token:
// http://localhost:8888/?token=f5ee846e9bd61c3a8d835ecd9b965591511a331417b997b7
let dataString = data.toString();
let urlMatch = dataString.match(/\[C[\s\S]+ {8}(.+:(\d+)\/.*)$/m);
if (urlMatch) {
// Legacy case: manually parse token info if no token/port were passed
return [vscode.Uri.parse(urlMatch[1]), urlMatch[2]];
} else if (this._uri && dataString.indexOf(JupyterStartedMessage) > -1) {
// Default case: detect the notebook started message, indicating our preferred port and token were used
return [this._uri, this._port];
}
return [undefined, undefined];
}
private notifyStarted(install: JupyterServerInstallation, jupyterUri: string): void {
install.outputChannel.appendLine(localize('jupyterOutputMsgStartSuccessful', '... Jupyter is running at {0}', jupyterUri));
}
private notify(install: JupyterServerInstallation, message: string): void {
install.outputChannel.appendLine(message);
}
private notifyStarting(install: JupyterServerInstallation, startCommand: string): void {
install.outputChannel.appendLine(localize('jupyterOutputMsgStart', '... Starting Notebook server'));
install.outputChannel.appendLine(` > ${startCommand}`);
}
private spawnJupyterProcess(install: JupyterServerInstallation, startCommand: string): ChildProcess {
// Specify the global environment variables
let env = this.getEnvWithConfigPaths();
// Setting the PATH variable here for the jupyter command. Apparently setting it above will cause the
// notebook process to die even though we don't override it with the for loop logic above.
delete env['Path'];
env['PATH'] = install.pythonEnvVarPath;
// 'MSHOST_TELEMETRY_ENABLED' and 'MSHOST_ENVIRONMENT' environment variables are set
// for telemetry purposes used by PROSE in the process where the Jupyter kernel runs
if (vscode.workspace.getConfiguration('telemetry').get<boolean>('enableTelemetry', true)) {
env['MSHOST_TELEMETRY_ENABLED'] = true;
} else {
env['MSHOST_TELEMETRY_ENABLED'] = false;
}
env['MSHOST_ENVIRONMENT'] = 'ADSClient-' + vscode.version;
// Start the notebook process
let options = {
shell: true,
env: env
};
let childProcess = this.utils.spawn(startCommand, [], options);
return childProcess;
}
private getEnvWithConfigPaths(): any {
let env = Object.assign({}, process.env);
env['JUPYTER_CONFIG_DIR'] = this.instanceConfigRoot;
env['JUPYTER_PATH'] = this.instanceDataRoot;
return env;
}
}
class ErrorHandler {
private numErrors: number = 0;
public handleError(error: Error): ErrorAction {
this.numErrors++;
return this.numErrors > 3 ? ErrorAction.Shutdown : ErrorAction.Continue;
}
}
enum ErrorAction {
Continue = 1,
Shutdown = 2
}

View File

@@ -0,0 +1,173 @@
'use strict';
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
import { window, OutputChannel } from 'vscode';
import * as Constants from '../common/constants';
import * as nodeUtil from 'util';
import PromptFactory from './factory';
import EscapeException from './escapeException';
import { IQuestion, IPrompter, IPromptCallback } from './question';
// Supports simple pattern for prompting for user input and acting on this
export default class CodeAdapter implements IPrompter {
private outChannel: OutputChannel;
private outBuffer: string = '';
private messageLevelFormatters = {};
constructor() {
// TODO Decide whether output channel logging should be saved here?
this.outChannel = window.createOutputChannel(Constants.outputChannelName);
// this.outChannel.clear();
}
public logError(message: any): void {
let line = `error: ${message.message}\n Code - ${message.code}`;
this.outBuffer += `${line}\n`;
this.outChannel.appendLine(line);
}
// private formatInfo(message: any) {
// const prefix = `${message.level}: (${message.id}) `;
// if (message.id === "json") {
// let jsonString = JSON.stringify(message.data, undefined, 4);
// return `${prefix}${message.message}\n${jsonString}`;
// }
// else {
// return `${prefix}${message.message}`;
// }
// }
// private formatAction(message: any) {
// const prefix = `info: ${message.level}: (${message.id}) `;
// return `${prefix}${message.message}`;
// }
private formatMessage(message: any): string {
const prefix = `${message.level}: (${message.id}) `;
return `${prefix}${message.message}`;
}
// private formatConflict(message: any) {
// var msg = message.message + ':\n';
// var picks = (<any[]>message.data.picks);
// var pickCount = 1;
// picks.forEach((pick) => {
// let pickMessage = (pickCount++).toString() + "). " + pick.endpoint.name + "#" + pick.endpoint.target;
// if (pick.pkgMeta._resolution && pick.pkgMeta._resolution.tag) {
// pickMessage += " which resolved to " + pick.pkgMeta._resolution.tag
// }
// if (Array.isArray(pick.dependants) && pick.dependants.length > 0) {
// pickMessage += " and is required by ";
// pick.dependants.forEach((dep) => {
// pickMessage += " " + dep.endpoint.name + "#" + dep.endpoint.target;
// });
// }
// msg += " " + pickMessage + "\n";
// });
// var prefix = (message.id === "solved"? "info" : "warn") + `: ${message.level}: (${message.id}) `;
// return prefix + msg;
// }
public log(message: any): void {
let line: string = '';
if (message && typeof (message.level) === 'string') {
let formatter: (a: any) => string = this.formatMessage;
if (this.messageLevelFormatters[message.level]) {
formatter = this.messageLevelFormatters[message.level];
}
line = formatter(message);
} else {
line = nodeUtil.format(arguments);
}
this.outBuffer += `${line}\n`;
this.outChannel.appendLine(line);
}
public clearLog(): void {
this.outChannel.clear();
}
public showLog(): void {
this.outChannel.show();
}
// TODO define question interface
private fixQuestion(question: any): any {
if (question.type === 'checkbox' && Array.isArray(question.choices)) {
// For some reason when there's a choice of checkboxes, they aren't formatted properly
// Not sure where the issue is
question.choices = question.choices.map(item => {
if (typeof (item) === 'string') {
return { checked: false, name: item, value: item };
} else {
return item;
}
});
}
}
public promptSingle<T>(question: IQuestion, ignoreFocusOut?: boolean): Promise<T> {
let questions: IQuestion[] = [question];
return this.prompt(questions, ignoreFocusOut).then((answers: { [key: string]: T }) => {
if (answers) {
let response: T = answers[question.name];
return response || undefined;
}
});
}
public prompt<T>(questions: IQuestion[], ignoreFocusOut?: boolean): Promise<{ [key: string]: T }> {
let answers: { [key: string]: T } = {};
// Collapse multiple questions into a set of prompt steps
let promptResult: Promise<{ [key: string]: T }> = questions.reduce((promise: Promise<{ [key: string]: T }>, question: IQuestion) => {
this.fixQuestion(question);
return promise.then(() => {
return PromptFactory.createPrompt(question, ignoreFocusOut);
}).then(prompt => {
// Original Code: uses jQuery patterns. Keeping for reference
// if (!question.when || question.when(answers) === true) {
// return prompt.render().then(result => {
// answers[question.name] = question.filter ? question.filter(result) : result;
// });
// }
if (!question.shouldPrompt || question.shouldPrompt(answers) === true) {
return prompt.render().then(result => {
answers[question.name] = result;
if (question.onAnswered) {
question.onAnswered(result);
}
return answers;
});
}
return answers;
});
}, Promise.resolve());
return promptResult.catch(err => {
if (err instanceof EscapeException || err instanceof TypeError) {
return undefined;
}
window.showErrorMessage(err.message);
});
}
// Helper to make it possible to prompt using callback pattern. Generally Promise is a preferred flow
public promptCallback(questions: IQuestion[], callback: IPromptCallback): void {
// Collapse multiple questions into a set of prompt steps
this.prompt(questions).then(answers => {
if (callback) {
callback(answers);
}
});
}
}

View File

@@ -0,0 +1,52 @@
'use strict';
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
import { window } from 'vscode';
import Prompt from './prompt';
import EscapeException from './escapeException';
const figures = require('figures');
export default class CheckboxPrompt extends Prompt {
constructor(question: any, ignoreFocusOut?: boolean) {
super(question, ignoreFocusOut);
}
public render(): any {
let choices = this._question.choices.reduce((result, choice) => {
let choiceName = choice.name || choice;
result[`${choice.checked === true ? figures.radioOn : figures.radioOff} ${choiceName}`] = choice;
return result;
}, {});
let options = this.defaultQuickPickOptions;
options.placeHolder = this._question.message;
let quickPickOptions = Object.keys(choices);
quickPickOptions.push(figures.tick);
return window.showQuickPick(quickPickOptions, options)
.then(result => {
if (result === undefined) {
throw new EscapeException();
}
if (result !== figures.tick) {
choices[result].checked = !choices[result].checked;
return this.render();
}
return this._question.choices.reduce((result2, choice) => {
if (choice.checked === true) {
result2.push(choice.value);
}
return result2;
}, []);
});
}
}

View File

@@ -0,0 +1,34 @@
'use strict';
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
import { window } from 'vscode';
import Prompt from './prompt';
import LocalizedConstants = require('../common/localizedConstants');
import EscapeException from './escapeException';
export default class ConfirmPrompt extends Prompt {
constructor(question: any, ignoreFocusOut?: boolean) {
super(question, ignoreFocusOut);
}
public render(): any {
let choices: { [id: string]: boolean } = {};
choices[LocalizedConstants.msgYes] = true;
choices[LocalizedConstants.msgNo] = false;
let options = this.defaultQuickPickOptions;
options.placeHolder = this._question.message;
return window.showQuickPick(Object.keys(choices), options)
.then(result => {
if (result === undefined) {
throw new EscapeException();
}
return choices[result] || false;
});
}
}

View File

@@ -0,0 +1,3 @@
'use strict';
export default require('error-ex')('EscapeException');

View File

@@ -0,0 +1,78 @@
'use strict';
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
import vscode = require('vscode');
import Prompt from './prompt';
import EscapeException from './escapeException';
import { INameValueChoice } from './question';
const figures = require('figures');
export default class ExpandPrompt extends Prompt {
constructor(question: any, ignoreFocusOut?: boolean) {
super(question, ignoreFocusOut);
}
public render(): any {
// label indicates this is a quickpick item. Otherwise it's a name-value pair
if (this._question.choices[0].label) {
return this.renderQuickPick(this._question.choices);
} else {
return this.renderNameValueChoice(this._question.choices);
}
}
private renderQuickPick(choices: vscode.QuickPickItem[]): any {
let options = this.defaultQuickPickOptions;
options.placeHolder = this._question.message;
return vscode.window.showQuickPick(choices, options)
.then(result => {
if (result === undefined) {
throw new EscapeException();
}
return this.validateAndReturn(result || false);
});
}
private renderNameValueChoice(choices: INameValueChoice[]): any {
const choiceMap = this._question.choices.reduce((result, choice) => {
result[choice.name] = choice.value;
return result;
}, {});
let options = this.defaultQuickPickOptions;
options.placeHolder = this._question.message;
return vscode.window.showQuickPick(Object.keys(choiceMap), options)
.then(result => {
if (result === undefined) {
throw new EscapeException();
}
// Note: cannot be used with 0 or false responses
let returnVal = choiceMap[result] || false;
return this.validateAndReturn(returnVal);
});
}
private validateAndReturn(value: any): any {
if (!this.validate(value)) {
return this.render();
}
return value;
}
private validate(value: any): boolean {
const validationError = this._question.validate ? this._question.validate(value || '') : undefined;
if (validationError) {
this._question.message = `${figures.warning} ${validationError}`;
return false;
}
return true;
}
}

View File

@@ -0,0 +1,39 @@
'use strict';
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
import Prompt from './prompt';
import InputPrompt from './input';
import PasswordPrompt from './password';
import ListPrompt from './list';
import ConfirmPrompt from './confirm';
import CheckboxPrompt from './checkbox';
import ExpandPrompt from './expand';
export default class PromptFactory {
public static createPrompt(question: any, ignoreFocusOut?: boolean): Prompt {
/**
* TODO:
* - folder
*/
switch (question.type || 'input') {
case 'string':
case 'input':
return new InputPrompt(question, ignoreFocusOut);
case 'password':
return new PasswordPrompt(question, ignoreFocusOut);
case 'list':
return new ListPrompt(question, ignoreFocusOut);
case 'confirm':
return new ConfirmPrompt(question, ignoreFocusOut);
case 'checkbox':
return new CheckboxPrompt(question, ignoreFocusOut);
case 'expand':
return new ExpandPrompt(question, ignoreFocusOut);
default:
throw new Error(`Could not find a prompt for question type ${question.type}`);
}
}
}

View File

@@ -0,0 +1,59 @@
'use strict';
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
import { window, InputBoxOptions } from 'vscode';
import Prompt from './prompt';
import EscapeException from './escapeException';
const figures = require('figures');
export default class InputPrompt extends Prompt {
protected _options: InputBoxOptions;
constructor(question: any, ignoreFocusOut?: boolean) {
super(question, ignoreFocusOut);
this._options = this.defaultInputBoxOptions;
this._options.prompt = this._question.message;
}
// Helper for callers to know the right type to get from the type factory
public static get promptType(): string { return 'input'; }
public render(): any {
// Prefer default over the placeHolder, if specified
let placeHolder = this._question.default ? this._question.default : this._question.placeHolder;
if (this._question.default instanceof Error) {
placeHolder = this._question.default.message;
this._question.default = undefined;
}
this._options.placeHolder = placeHolder;
return window.showInputBox(this._options)
.then(result => {
if (result === undefined) {
throw new EscapeException();
}
if (result === '') {
// Use the default value, if defined
result = this._question.default || '';
}
const validationError = this._question.validate ? this._question.validate(result || '') : undefined;
if (validationError) {
this._question.default = new Error(`${figures.warning} ${validationError}`);
return this.render();
}
return result;
});
}
}

View File

@@ -0,0 +1,34 @@
'use strict';
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
import { window } from 'vscode';
import Prompt from './prompt';
import EscapeException from './escapeException';
export default class ListPrompt extends Prompt {
constructor(question: any, ignoreFocusOut?: boolean) {
super(question, ignoreFocusOut);
}
public render(): any {
const choices = this._question.choices.reduce((result, choice) => {
result[choice.name] = choice.value;
return result;
}, {});
let options = this.defaultQuickPickOptions;
options.placeHolder = this._question.message;
return window.showQuickPick(Object.keys(choices), options)
.then(result => {
if (result === undefined) {
throw new EscapeException();
}
return choices[result];
});
}
}

View File

@@ -0,0 +1,14 @@
'use strict';
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
import InputPrompt from './input';
export default class PasswordPrompt extends InputPrompt {
constructor(question: any, ignoreFocusOut?: boolean) {
super(question, ignoreFocusOut);
this._options.password = true;
}
}

View File

@@ -0,0 +1,70 @@
'use strict';
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
import { window, StatusBarItem, StatusBarAlignment } from 'vscode';
export default class ProgressIndicator {
private _statusBarItem: StatusBarItem;
constructor() {
this._statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left);
}
private _tasks: string[] = [];
public beginTask(task: string): void {
this._tasks.push(task);
this.displayProgressIndicator();
}
public endTask(task: string): void {
if (this._tasks.length > 0) {
this._tasks.pop();
}
this.setMessage();
}
private setMessage(): void {
if (this._tasks.length === 0) {
this._statusBarItem.text = '';
this.hideProgressIndicator();
return;
}
this._statusBarItem.text = this._tasks[this._tasks.length - 1];
this._statusBarItem.show();
}
private _interval: any;
private displayProgressIndicator(): void {
this.setMessage();
this.hideProgressIndicator();
this._interval = setInterval(() => this.onDisplayProgressIndicator(), 100);
}
private hideProgressIndicator(): void {
if (this._interval) {
clearInterval(this._interval);
this._interval = undefined;
}
this.ProgressCounter = 0;
}
private ProgressText = ['|', '/', '-', '\\', '|', '/', '-', '\\'];
private ProgressCounter = 0;
private onDisplayProgressIndicator(): void {
if (this._tasks.length === 0) {
return;
}
let txt = this.ProgressText[this.ProgressCounter];
this._statusBarItem.text = this._tasks[this._tasks.length - 1] + ' ' + txt;
this.ProgressCounter++;
if (this.ProgressCounter >= this.ProgressText.length - 1) {
this.ProgressCounter = 0;
}
}
}

View File

@@ -0,0 +1,33 @@
'use strict';
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
import { InputBoxOptions, QuickPickOptions } from 'vscode';
abstract class Prompt {
protected _question: any;
protected _ignoreFocusOut?: boolean;
constructor(question: any, ignoreFocusOut?: boolean) {
this._question = question;
this._ignoreFocusOut = ignoreFocusOut ? ignoreFocusOut : false;
}
public abstract render(): any;
protected get defaultQuickPickOptions(): QuickPickOptions {
return {
ignoreFocusOut: this._ignoreFocusOut
};
}
protected get defaultInputBoxOptions(): InputBoxOptions {
return {
ignoreFocusOut: this._ignoreFocusOut
};
}
}
export default Prompt;

View File

@@ -0,0 +1,68 @@
'use strict';
import vscode = require('vscode');
export class QuestionTypes {
public static get input(): string { return 'input'; }
public static get password(): string { return 'password'; }
public static get list(): string { return 'list'; }
public static get confirm(): string { return 'confirm'; }
public static get checkbox(): string { return 'checkbox'; }
public static get expand(): string { return 'expand'; }
}
// Question interface to clarify how to use the prompt feature
// based on Bower Question format: https://github.com/bower/bower/blob/89069784bb46bfd6639b4a75e98a0d7399a8c2cb/packages/bower-logger/README.md
export interface IQuestion {
// Type of question (see QuestionTypes)
type: string;
// Name of the question for disambiguation
name: string;
// Message to display to the user
message: string;
// Optional placeHolder to give more detailed information to the user
placeHolder?: any;
// Optional default value - this will be used instead of placeHolder
default?: any;
// optional set of choices to be used. Can be QuickPickItems or a simple name-value pair
choices?: Array<vscode.QuickPickItem | INameValueChoice>;
// Optional validation function that returns an error string if validation fails
validate?: (value: any) => string;
// Optional pre-prompt function. Takes in set of answers so far, and returns true if prompt should occur
shouldPrompt?: (answers: { [id: string]: any }) => boolean;
// Optional action to take on the question being answered
onAnswered?: (value: any) => void;
// Optional set of options to support matching choices.
matchOptions?: vscode.QuickPickOptions;
}
// Pair used to display simple choices to the user
export interface INameValueChoice {
name: string;
value: any;
}
// Generic object that can be used to define a set of questions and handle the result
export interface IQuestionHandler {
// Set of questions to be answered
questions: IQuestion[];
// Optional callback, since questions may handle themselves
callback?: IPromptCallback;
}
export interface IPrompter {
promptSingle<T>(question: IQuestion, ignoreFocusOut?: boolean): Promise<T>;
/**
* Prompts for multiple questions
*
* @returns {[questionId: string]: T} Map of question IDs to results, or undefined if
* the user canceled the question session
*/
prompt<T>(questions: IQuestion[], ignoreFocusOut?: boolean): Promise<{ [questionId: string]: any }>;
promptCallback(questions: IQuestion[], callback: IPromptCallback): void;
}
export interface IPromptCallback {
(answers: { [id: string]: any }): void;
}

View File

@@ -0,0 +1,259 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as vscode from 'vscode';
import { IServerInstance } from '../jupyter/common';
import { Session, Kernel, KernelMessage, ServerConnection } from '@jupyterlab/services';
import { ISignal } from '@phosphor/signaling';
export class JupyterServerInstanceStub implements IServerInstance {
public get port(): string {
return undefined;
}
public get uri(): vscode.Uri {
return undefined;
}
public configure(): Promise<void> {
throw new Error('Method not implemented.');
}
public start(): Promise<void> {
throw new Error('Method not implemented.');
}
stop(): Promise<void> {
throw new Error('Method not implemented.');
}
}
//#region sesion and kernel stubs (long)
export class SessionStub implements Session.ISession {
public get terminated(): ISignal<this, void> {
throw new Error('Method not implemented.');
}
public get kernelChanged(): ISignal<this, Session.IKernelChangedArgs> {
throw new Error('Method not implemented.');
}
public get statusChanged(): ISignal<this, Kernel.Status> {
throw new Error('Method not implemented.');
}
public get propertyChanged(): ISignal<this, 'path' | 'name' | 'type'> {
throw new Error('Method not implemented.');
}
public get iopubMessage(): ISignal<this, KernelMessage.IIOPubMessage> {
throw new Error('Method not implemented.');
}
public get unhandledMessage(): ISignal<this, KernelMessage.IMessage> {
throw new Error('Method not implemented.');
}
public get anyMessage(): ISignal<this, Kernel.IAnyMessageArgs> {
throw new Error('Method not implemented.');
}
public get id(): string {
throw new Error('Method not implemented.');
}
public get path(): string {
throw new Error('Method not implemented.');
}
public get name(): string {
throw new Error('Method not implemented.');
}
public get type(): string {
throw new Error('Method not implemented.');
}
public get serverSettings(): ServerConnection.ISettings {
throw new Error('Method not implemented.');
}
public get model(): Session.IModel {
throw new Error('Method not implemented.');
}
public get kernel(): Kernel.IKernelConnection {
throw new Error('Method not implemented.');
}
public get status(): Kernel.Status {
throw new Error('Method not implemented.');
}
public get isDisposed(): boolean {
throw new Error('Method not implemented.');
}
setPath(path: string): Promise<void> {
throw new Error('Method not implemented.');
}
setName(name: string): Promise<void> {
throw new Error('Method not implemented.');
}
setType(type: string): Promise<void> {
throw new Error('Method not implemented.');
}
changeKernel(options: Partial<Kernel.IModel>): Promise<Kernel.IKernelConnection> {
throw new Error('Method not implemented.');
}
shutdown(): Promise<void> {
throw new Error('Method not implemented.');
}
dispose(): void {
throw new Error('Method not implemented.');
}
}
export class KernelStub implements Kernel.IKernel {
get terminated(): ISignal<this, void> {
throw new Error('Method not implemented.');
}
get statusChanged(): ISignal<this, Kernel.Status> {
throw new Error('Method not implemented.');
}
get iopubMessage(): ISignal<this, KernelMessage.IIOPubMessage> {
throw new Error('Method not implemented.');
}
get unhandledMessage(): ISignal<this, KernelMessage.IMessage> {
throw new Error('Method not implemented.');
}
get anyMessage(): ISignal<this, Kernel.IAnyMessageArgs> {
throw new Error('Method not implemented.');
}
get serverSettings(): ServerConnection.ISettings {
throw new Error('Method not implemented.');
}
get id(): string {
throw new Error('Method not implemented.');
}
get name(): string {
throw new Error('Method not implemented.');
}
get model(): Kernel.IModel {
throw new Error('Method not implemented.');
}
get username(): string {
throw new Error('Method not implemented.');
}
get clientId(): string {
throw new Error('Method not implemented.');
}
get status(): Kernel.Status {
throw new Error('Method not implemented.');
}
get info(): KernelMessage.IInfoReply {
throw new Error('Method not implemented.');
}
get isReady(): boolean {
throw new Error('Method not implemented.');
}
get ready(): Promise<void> {
throw new Error('Method not implemented.');
}
get isDisposed(): boolean {
throw new Error('Method not implemented.');
}
shutdown(): Promise<void> {
throw new Error('Method not implemented.');
}
getSpec(): Promise<Kernel.ISpecModel> {
throw new Error('Method not implemented.');
}
sendShellMessage(msg: KernelMessage.IShellMessage, expectReply?: boolean, disposeOnDone?: boolean): Kernel.IFuture {
throw new Error('Method not implemented.');
}
reconnect(): Promise<void> {
throw new Error('Method not implemented.');
}
interrupt(): Promise<void> {
throw new Error('Method not implemented.');
}
restart(): Promise<void> {
throw new Error('Method not implemented.');
}
requestKernelInfo(): Promise<KernelMessage.IInfoReplyMsg> {
throw new Error('Method not implemented.');
}
requestComplete(content: KernelMessage.ICompleteRequest): Promise<KernelMessage.ICompleteReplyMsg> {
throw new Error('Method not implemented.');
}
requestInspect(content: KernelMessage.IInspectRequest): Promise<KernelMessage.IInspectReplyMsg> {
throw new Error('Method not implemented.');
}
requestHistory(content: KernelMessage.IHistoryRequest): Promise<KernelMessage.IHistoryReplyMsg> {
throw new Error('Method not implemented.');
}
requestExecute(content: KernelMessage.IExecuteRequest, disposeOnDone?: boolean): Kernel.IFuture {
throw new Error('Method not implemented.');
}
requestIsComplete(content: KernelMessage.IIsCompleteRequest): Promise<KernelMessage.IIsCompleteReplyMsg> {
throw new Error('Method not implemented.');
}
requestCommInfo(content: KernelMessage.ICommInfoRequest): Promise<KernelMessage.ICommInfoReplyMsg> {
throw new Error('Method not implemented.');
}
sendInputReply(content: KernelMessage.IInputReply): void {
throw new Error('Method not implemented.');
}
connectToComm(targetName: string, commId?: string): Kernel.IComm {
throw new Error('Method not implemented.');
}
registerCommTarget(targetName: string, callback: (comm: Kernel.IComm, msg: KernelMessage.ICommOpenMsg) => void | PromiseLike<void>): void {
throw new Error('Method not implemented.');
}
removeCommTarget(targetName: string, callback: (comm: Kernel.IComm, msg: KernelMessage.ICommOpenMsg) => void | PromiseLike<void>): void {
throw new Error('Method not implemented.');
}
registerMessageHook(msgId: string, hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>): void {
throw new Error('Method not implemented.');
}
removeMessageHook(msgId: string, hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>): void {
throw new Error('Method not implemented.');
}
dispose(): void {
throw new Error('Method not implemented.');
}
}
export class FutureStub implements Kernel.IFuture {
get msg(): KernelMessage.IShellMessage {
throw new Error('Method not implemented.');
}
get done(): Promise<KernelMessage.IShellMessage> {
throw new Error('Method not implemented.');
}
get isDisposed(): boolean {
throw new Error('Method not implemented.');
}
get onReply(): (msg: KernelMessage.IShellMessage) => void | PromiseLike<void> {
throw new Error('Method not implemented.');
}
set onReply(handler: (msg: KernelMessage.IShellMessage) => void | PromiseLike<void>) {
throw new Error('Method not implemented.');
}
get onStdin(): (msg: KernelMessage.IStdinMessage) => void | PromiseLike<void> {
throw new Error('Method not implemented.');
}
set onStdin(handler: (msg: KernelMessage.IStdinMessage) => void | PromiseLike<void>) {
throw new Error('Method not implemented.');
}
get onIOPub(): (msg: KernelMessage.IIOPubMessage) => void | PromiseLike<void> {
throw new Error('Method not implemented.');
}
set onIOPub(handler: (msg: KernelMessage.IIOPubMessage) => void | PromiseLike<void>) {
throw new Error('Method not implemented.');
}
registerMessageHook(hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>): void {
throw new Error('Method not implemented.');
}
removeMessageHook(hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>): void {
throw new Error('Method not implemented.');
}
sendInputReply(content: KernelMessage.IInputReply): void {
throw new Error('Method not implemented.');
}
dispose(): void {
throw new Error('Method not implemented.');
}
}
//#endregion

View File

@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// This code is originally from https://github.com/Microsoft/vscode/blob/master/src/vs/base/test/node/port.test.ts
'use strict';
import * as assert from 'assert';
import * as net from 'net';
import 'mocha';
import * as ports from '../../common/ports';
describe('Ports', () => {
it('Should Find a free port (no timeout)', function (done): void {
this.timeout(1000 * 10); // higher timeout for this test
// get an initial freeport >= 7000
ports.findFreePort(7000, 100, 300000).then(initialPort => {
assert.ok(initialPort >= 7000);
// create a server to block this port
const server = net.createServer();
server.listen(initialPort, undefined, undefined, () => {
// once listening, find another free port and assert that the port is different from the opened one
ports.findFreePort(7000, 50, 300000).then(freePort => {
assert.ok(freePort >= 7000 && freePort !== initialPort);
server.close();
done();
}, err => done(err));
});
}, err => done(err));
});
it('Should Find a free port in strict mode', function (done): void {
this.timeout(1000 * 10); // higher timeout for this test
// get an initial freeport >= 7000
let options = new ports.StrictPortFindOptions(7000, 7100, 7200);
options.timeout = 300000;
ports.strictFindFreePort(options).then(initialPort => {
assert.ok(initialPort >= 7000);
// create a server to block this port
const server = net.createServer();
server.listen(initialPort, undefined, undefined, () => {
// once listening, find another free port and assert that the port is different from the opened one
options.startPort = initialPort;
options.maxRetriesPerStartPort = 1;
options.totalRetryLoops = 50;
ports.strictFindFreePort(options).then(freePort => {
assert.ok(freePort >= 7100 && freePort !== initialPort);
server.close();
done();
}, err => done(err));
});
}, err => done(err));
});
});

View File

@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as should from 'should';
import 'mocha';
import * as notebookUtils from '../../common/notebookUtils';
describe('Random Token', () => {
it('Should have default length and be hex only', async function (): Promise<void> {
let token = await notebookUtils.getRandomToken();
should(token).have.length(48);
let validChars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
for (let i = 0; i < token.length; i++) {
let char = token.charAt(i);
should(validChars.indexOf(char)).be.greaterThan(-1);
}
});
});

View File

@@ -0,0 +1,46 @@
'use strict';
import * as vscode from 'vscode';
export class MockExtensionContext implements vscode.ExtensionContext {
logger: undefined;
logDirectory: './';
subscriptions: { dispose(): any; }[];
workspaceState: vscode.Memento;
globalState: vscode.Memento;
extensionPath: string;
asAbsolutePath(relativePath: string): string {
return relativePath;
}
storagePath: string;
constructor() {
this.subscriptions = [];
}
}
export class MockOutputChannel implements vscode.OutputChannel {
name: string;
append(value: string): void {
throw new Error('Method not implemented.');
}
appendLine(value: string): void {
throw new Error('Method not implemented.');
}
clear(): void {
throw new Error('Method not implemented.');
}
show(preserveFocus?: boolean): void;
show(column?: vscode.ViewColumn, preserveFocus?: boolean): void;
show(column?: any, preserveFocus?: any): void {
throw new Error('Method not implemented.');
}
hide(): void {
throw new Error('Method not implemented.');
}
dispose(): void {
throw new Error('Method not implemented.');
}
}

View File

@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
export async function assertThrowsAsync(fn, regExp): Promise<void> {
let f = () => {
// Empty
};
try {
await fn();
} catch (e) {
f = () => { throw e; };
} finally {
assert.throws(f, regExp);
}
}

View File

@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// import * as vscode from 'vscode';
// import { context } from './testContext';
const path = require('path');
const testRunner = require('vscode/lib/testrunner');
const suite = 'Notebook Tests';
const options: any = {
ui: 'bdd',
useColors: true,
timeout: 600000
};
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
options.reporter = 'mocha-multi-reporters';
options.reporterOptions = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
testsuitesTitle: `${suite} ${process.platform}`,
mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
}
};
}
testRunner.configure(options);
export = testRunner;

View File

@@ -0,0 +1,97 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as vscode from 'vscode';
import * as should from 'should';
import * as TypeMoq from 'typemoq';
import * as path from 'path';
import { ContentsManager, Contents } from '@jupyterlab/services';
import { nb } from 'sqlops';
import 'mocha';
import { INotebook, CellTypes } from '../../contracts/content';
import { RemoteContentManager } from '../../jupyter/remoteContentManager';
import * as testUtils from '../common/testUtils';
let expectedNotebookContent: INotebook = {
cells: [{
cell_type: CellTypes.Code,
source: 'insert into t1 values (c1, c2)',
metadata: { language: 'python' },
execution_count: 1
}],
metadata: {
kernelspec: {
name: 'mssql',
language: 'sql'
}
},
nbformat: 5,
nbformat_minor: 0
};
let notebookContentString = JSON.stringify(expectedNotebookContent);
function verifyMatchesExpectedNotebook(notebook: nb.INotebookContents): void {
should(notebook.cells).have.length(1, 'Expected 1 cell');
should(notebook.cells[0].cell_type).equal(CellTypes.Code);
should(notebook.cells[0].source).equal(expectedNotebookContent.cells[0].source);
should(notebook.metadata.kernelspec.name).equal(expectedNotebookContent.metadata.kernelspec.name);
should(notebook.nbformat).equal(expectedNotebookContent.nbformat);
should(notebook.nbformat_minor).equal(expectedNotebookContent.nbformat_minor);
}
describe('Remote Content Manager', function (): void {
let mockJupyterManager = TypeMoq.Mock.ofType(ContentsManager);
let contentManager = new RemoteContentManager(mockJupyterManager.object);
// TODO re-enable when we bring in usage of remote content managers / binders
// it('Should return undefined if path is undefined', async function(): Promise<void> {
// let content = await contentManager.getNotebookContents(undefined);
// should(content).be.undefined();
// // tslint:disable-next-line:no-null-keyword
// content = await contentManager.getNotebookContents(null);
// should(content).be.undefined();
// content = await contentManager.getNotebookContents(vscode.Uri.file(''));
// should(content).be.undefined();
// });
it('Should throw if API call throws', async function (): Promise<void> {
let exception = new Error('Path was wrong');
mockJupyterManager.setup(c => c.get(TypeMoq.It.isAny(), TypeMoq.It.isAny())).throws(exception);
await testUtils.assertThrowsAsync(async () => await contentManager.getNotebookContents(vscode.Uri.file('/path/doesnot/exist.ipynb')), undefined);
});
it('Should return notebook contents parsed as INotebook when valid notebook file parsed', async function (): Promise<void> {
// Given a valid request to the notebook server
let remotePath = '/remote/path/that/exists.ipynb';
let contentsModel: Contents.IModel = {
name: path.basename(remotePath),
content: expectedNotebookContent,
path: remotePath,
type: 'notebook',
writable: false,
created: undefined,
last_modified: undefined,
mimetype: 'json',
format: 'json'
};
mockJupyterManager.setup(c => c.get(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(contentsModel));
// when I read the content
let notebook = await contentManager.getNotebookContents(vscode.Uri.file(remotePath));
// then I expect notebook format to match
verifyMatchesExpectedNotebook(notebook);
});
it('Should return undefined if service does not return anything', async function (): Promise<void> {
// Given a valid request to the notebook server
let remotePath = '/remote/path/that/does/not/exist.ipynb';
mockJupyterManager.setup(c => c.get(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined));
// when I read the content
let notebook = await contentManager.getNotebookContents(vscode.Uri.file(remotePath));
// then I expect notebook format to match
should(notebook).be.undefined();
});
});

View File

@@ -0,0 +1,203 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as should from 'should';
import * as TypeMoq from 'typemoq';
import { nb } from 'sqlops';
import { Kernel, KernelMessage } from '@jupyterlab/services';
import 'mocha';
import { KernelStub, FutureStub } from '../common';
import { JupyterKernel, JupyterFuture } from '../../jupyter/jupyterKernel';
describe('Jupyter Session', function (): void {
let mockJupyterKernel: TypeMoq.IMock<KernelStub>;
let kernel: JupyterKernel;
beforeEach(() => {
mockJupyterKernel = TypeMoq.Mock.ofType(KernelStub);
kernel = new JupyterKernel(mockJupyterKernel.object);
});
it('should pass through most properties', function (done): void {
// Given values for the passthrough properties
mockJupyterKernel.setup(s => s.id).returns(() => 'id');
mockJupyterKernel.setup(s => s.name).returns(() => 'name');
mockJupyterKernel.setup(s => s.isReady).returns(() => true);
let readyPromise = Promise.reject('err');
mockJupyterKernel.setup(s => s.ready).returns(() => readyPromise);
// Should return those values when called
should(kernel.id).equal('id');
should(kernel.name).equal('name');
should(kernel.isReady).be.true();
kernel.ready.then((fulfilled) => done('Err: should not succeed'), (err) => done());
});
it('should passthrough spec with expected name and display name', async function (): Promise<void> {
let spec: Kernel.ISpecModel = {
name: 'python',
display_name: 'Python 3',
language: 'python',
argv: undefined,
resources: undefined
};
mockJupyterKernel.setup(k => k.getSpec()).returns(() => Promise.resolve(spec));
let actualSpec = await kernel.getSpec();
should(actualSpec.name).equal('python');
should(actualSpec.display_name).equal('Python 3');
});
it('should return code completions on requestComplete', async function (): Promise<void> {
should(kernel.supportsIntellisense).be.true();
let completeMsg: KernelMessage.ICompleteReplyMsg = {
channel: 'shell',
content: {
cursor_start: 0,
cursor_end: 2,
matches: ['print'],
metadata: {},
status: 'ok'
},
header: undefined,
metadata: undefined,
parent_header: undefined
};
mockJupyterKernel.setup(k => k.requestComplete(TypeMoq.It.isAny())).returns(() => Promise.resolve(completeMsg));
let msg = await kernel.requestComplete({
code: 'pr',
cursor_pos: 2
});
should(msg.type).equal('shell');
should(msg.content).equal(completeMsg.content);
});
it('should return a simple future on requestExecute', async function (): Promise<void> {
let futureMock = TypeMoq.Mock.ofType(FutureStub);
const code = 'print("hello")';
let msg: KernelMessage.IShellMessage = {
channel: 'shell',
content: { code: code },
header: undefined,
metadata: undefined,
parent_header: undefined
};
futureMock.setup(f => f.msg).returns(() => msg);
let executeRequest: KernelMessage.IExecuteRequest;
let shouldDispose: KernelMessage.IExecuteRequest;
mockJupyterKernel.setup(k => k.requestExecute(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((request, disposeOnDone) => {
executeRequest = request;
shouldDispose = disposeOnDone;
return futureMock.object;
});
// When I request execute
let future = kernel.requestExecute({
code: code
}, true);
// Then expect wrapper to be returned
should(future).be.instanceof(JupyterFuture);
should(future.msg.type).equal('shell');
should(future.msg.content.code).equal(code);
should(executeRequest.code).equal(code);
should(shouldDispose).be.true();
});
});
describe('Jupyter Future', function (): void {
let mockJupyterFuture: TypeMoq.IMock<FutureStub>;
let future: JupyterFuture;
beforeEach(() => {
mockJupyterFuture = TypeMoq.Mock.ofType(FutureStub);
future = new JupyterFuture(mockJupyterFuture.object);
});
it('should return message on done', async function (): Promise<void> {
let msg: KernelMessage.IShellMessage = {
channel: 'shell',
content: { code: 'exec' },
header: undefined,
metadata: undefined,
parent_header: undefined
};
mockJupyterFuture.setup(f => f.done).returns(() => Promise.resolve(msg));
let actualMsg = await future.done;
should(actualMsg.content.code).equal('exec');
});
it('should relay reply message', async function (): Promise<void> {
let handler: (msg: KernelMessage.IShellMessage) => void | PromiseLike<void>;
mockJupyterFuture.setup(f => f.onReply = TypeMoq.It.isAny()).callback(h => handler = h);
// When I set a reply handler and a message is sent
let msg: nb.IShellMessage;
future.setReplyHandler({
handle: (message => {
msg = message;
})
});
should(handler).not.be.undefined();
verifyRelayMessage('shell', handler, () => msg);
});
it('should relay StdIn message', async function (): Promise<void> {
let handler: (msg: KernelMessage.IStdinMessage) => void | PromiseLike<void>;
mockJupyterFuture.setup(f => f.onStdin = TypeMoq.It.isAny()).callback(h => handler = h);
// When I set a reply handler and a message is sent
let msg: nb.IStdinMessage;
future.setStdInHandler({
handle: (message => {
msg = message;
})
});
should(handler).not.be.undefined();
verifyRelayMessage('stdin', handler, () => msg);
});
it('should relay IOPub message', async function (): Promise<void> {
let handler: (msg: KernelMessage.IIOPubMessage) => void | PromiseLike<void>;
mockJupyterFuture.setup(f => f.onIOPub = TypeMoq.It.isAny()).callback(h => handler = h);
// When I set a reply handler and a message is sent
let msg: nb.IIOPubMessage;
future.setIOPubHandler({
handle: (message => {
msg = message;
})
});
should(handler).not.be.undefined();
verifyRelayMessage('iopub', handler, () => msg);
});
function verifyRelayMessage(channel: nb.Channel | KernelMessage.Channel, handler: (msg: KernelMessage.IMessage) => void | PromiseLike<void>, getMessage: () => nb.IMessage): void {
handler({
channel: <any>channel,
content: { value: 'test' },
metadata: { value: 'test' },
header: { username: 'test', version: '1', msg_id: undefined, msg_type: undefined, session: undefined },
parent_header: { username: 'test', version: '1', msg_id: undefined, msg_type: undefined, session: undefined }
});
let msg = getMessage();
// Then the value should be relayed
should(msg.type).equal(channel);
should(msg.content).have.property('value', 'test');
should(msg.metadata).have.property('value', 'test');
should(msg.header).have.property('username', 'test');
should(msg.parent_header).have.property('username', 'test');
}
});

View File

@@ -0,0 +1,236 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as should from 'should';
import * as TypeMoq from 'typemoq';
import * as vscode from 'vscode';
import * as stream from 'stream';
import { ChildProcess } from 'child_process';
import 'mocha';
import JupyterServerInstallation from '../../jupyter/jupyterServerInstallation';
import { ApiWrapper } from '../..//common/apiWrapper';
import { PerNotebookServerInstance, ServerInstanceUtils } from '../../jupyter/serverInstance';
import { MockOutputChannel } from '../common/stubs';
import * as testUtils from '../common/testUtils';
import { LocalJupyterServerManager } from '../../jupyter/jupyterServerManager';
const successMessage = `[I 14:00:38.811 NotebookApp] The Jupyter Notebook is running at:
[I 14:00:38.812 NotebookApp] http://localhost:8891/?token=...
[I 14:00:38.812 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
`;
describe('Jupyter server instance', function (): void {
let expectedPath = 'mydir/notebook.ipynb';
let mockInstall: TypeMoq.IMock<JupyterServerInstallation>;
let mockOutputChannel: TypeMoq.IMock<MockOutputChannel>;
let mockApiWrapper: TypeMoq.IMock<ApiWrapper>;
let mockUtils: TypeMoq.IMock<ServerInstanceUtils>;
let serverInstance: PerNotebookServerInstance;
beforeEach(() => {
mockApiWrapper = TypeMoq.Mock.ofType(ApiWrapper);
mockApiWrapper.setup(a => a.showErrorMessage(TypeMoq.It.isAny()));
mockApiWrapper.setup(a => a.getWorkspacePathFromUri(TypeMoq.It.isAny())).returns(() => undefined);
mockInstall = TypeMoq.Mock.ofType(JupyterServerInstallation, undefined, undefined, '/root');
mockOutputChannel = TypeMoq.Mock.ofType(MockOutputChannel);
mockInstall.setup(i => i.outputChannel).returns(() => mockOutputChannel.object);
mockInstall.setup(i => i.pythonExecutable).returns(() => 'python3');
mockUtils = TypeMoq.Mock.ofType(ServerInstanceUtils);
mockUtils.setup(u => u.checkProcessDied(TypeMoq.It.isAny())).returns(() => undefined);
serverInstance = new PerNotebookServerInstance({
documentPath: expectedPath,
install: mockInstall.object
}, mockUtils.object);
});
it('Should not be started initially', function (): void {
// Given a new instance It should not be started
should(serverInstance.isStarted).be.false();
should(serverInstance.port).be.undefined();
});
it('Should create config and data directories on configure', async function (): Promise<void> {
// Given a server instance
mockUtils.setup(u => u.mkDir(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => Promise.resolve());
mockUtils.setup(u => u.copy(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns(() => Promise.resolve());
mockUtils.setup(u => u.existsSync(TypeMoq.It.isAnyString())).returns(() => false);
// When I run configure
await serverInstance.configure();
// Then I expect a folder to have been created with config and data subdirs
mockUtils.verify(u => u.mkDir(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(5));
mockUtils.verify(u => u.copy(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(3));
mockUtils.verify(u => u.existsSync(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(1));
});
it('Should have URI info after start', async function (): Promise<void> {
// Given startup will succeed
let process = setupSpawn({
sdtout: (listener) => undefined,
stderr: (listener) => listener(successMessage)
});
mockUtils.setup(u => u.spawn(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => <ChildProcess>process.object);
// When I call start
await serverInstance.start();
// Then I expect all parts of the URI to be defined
should(serverInstance.uri).not.be.undefined();
should(serverInstance.uri.scheme).equal('http');
let settings = LocalJupyterServerManager.getLocalConnectionSettings(serverInstance.uri);
// Verify a token with expected length was generated
should(settings.token).have.length(48);
let hostAndPort = serverInstance.uri.authority.split(':');
// verify port was set as expected
should(hostAndPort[1]).length(4);
// And I expect it to be started
should(serverInstance.isStarted).be.true();
// And I expect listeners to be cleaned up
process.verify(p => p.on(TypeMoq.It.isValue('error'), TypeMoq.It.isAny()), TypeMoq.Times.once());
process.verify(p => p.on(TypeMoq.It.isValue('exit'), TypeMoq.It.isAny()), TypeMoq.Times.once());
});
it('Should throw if error before startup', async function (): Promise<void> {
let error = 'myerr';
let process = setupSpawn({
sdtout: (listener) => undefined,
stderr: (listener) => listener(successMessage),
error: (listener) => setTimeout(() => listener(new Error(error)), 10)
});
mockUtils.setup(u => u.spawn(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => <ChildProcess>process.object);
// When I call start then I expect it to pass
await serverInstance.start();
});
it('Should throw if exit before startup', async function (): Promise<void> {
let code = -1234;
let process = setupSpawn({
exit: (listener) => listener(code)
});
mockUtils.setup(u => u.spawn(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => <ChildProcess>process.object);
// When I call start then I expect the error to be thrown
await testUtils.assertThrowsAsync(() => serverInstance.start(), undefined);
should(serverInstance.isStarted).be.false();
});
it('Should call stop with correct port on close', async function (): Promise<void> {
// Given startup will succeed
let process = setupSpawn({
sdtout: (listener) => undefined,
stderr: (listener) => listener(successMessage)
});
mockUtils.setup(u => u.spawn(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => <ChildProcess>process.object);
let actualCommand: string = undefined;
mockUtils.setup(u => u.executeBufferedCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns((cmd) => {
actualCommand = cmd;
return Promise.resolve(undefined);
});
mockUtils.setup(u => u.pathExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(false));
mockUtils.setup(u => u.removeDir(TypeMoq.It.isAny())).returns(() => Promise.resolve());
// When I call start and then stop
await serverInstance.start();
await serverInstance.stop();
// Then I expect stop to be called on the child process
should(actualCommand.indexOf(`jupyter notebook stop ${serverInstance.port}`)).be.greaterThan(-1);
mockUtils.verify(u => u.removeDir(TypeMoq.It.isAny()), TypeMoq.Times.never());
});
it('Should remove directory on close', async function (): Promise<void> {
// Given configure and startup are done
mockUtils.setup(u => u.mkDir(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => Promise.resolve());
mockUtils.setup(u => u.copy(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns(() => Promise.resolve());
let process = setupSpawn({
sdtout: (listener) => undefined,
stderr: (listener) => listener(successMessage)
});
mockUtils.setup(u => u.spawn(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => <ChildProcess>process.object);
mockUtils.setup(u => u.executeBufferedCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns((cmd) => Promise.resolve(undefined));
mockUtils.setup(u => u.pathExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(true));
mockUtils.setup(u => u.removeDir(TypeMoq.It.isAny())).returns(() => Promise.resolve());
await serverInstance.configure();
await serverInstance.start();
// When I call stop
await serverInstance.stop();
// Then I expect the directory to be cleaned up
mockUtils.verify(u => u.removeDir(TypeMoq.It.isAny()), TypeMoq.Times.once());
});
function setupSpawn(callbacks: IProcessCallbacks): TypeMoq.IMock<ChildProcessStub> {
let stdoutMock = TypeMoq.Mock.ofType(stream.Readable);
stdoutMock.setup(s => s.on(TypeMoq.It.isValue('data'), TypeMoq.It.isAny()))
.returns((event, listener) => runIfExists(listener, callbacks.sdtout));
let stderrMock = TypeMoq.Mock.ofType(stream.Readable);
stderrMock.setup(s => s.on(TypeMoq.It.isValue('data'), TypeMoq.It.isAny()))
.returns((event, listener) => runIfExists(listener, callbacks.stderr));
let mockProcess = TypeMoq.Mock.ofType(ChildProcessStub);
mockProcess.setup(p => p.stdout).returns(() => stdoutMock.object);
mockProcess.setup(p => p.stderr).returns(() => stderrMock.object);
mockProcess.setup(p => p.on(TypeMoq.It.isValue('exit'), TypeMoq.It.isAny()))
.returns((event, listener) => runIfExists(listener, callbacks.exit));
mockProcess.setup(p => p.on(TypeMoq.It.isValue('error'), TypeMoq.It.isAny()))
.returns((event, listener) => runIfExists(listener, callbacks.error));
mockProcess.setup(p => p.removeListener(TypeMoq.It.isAny(), TypeMoq.It.isAny()));
mockProcess.setup(p => p.addListener(TypeMoq.It.isAny(), TypeMoq.It.isAny()));
return mockProcess;
}
function runIfExists(listener: any, callback: Function, delay: number = 5): stream.Readable {
setTimeout(() => {
if (callback) {
callback(listener);
}
}, delay);
return undefined;
}
});
interface IProcessCallbacks {
sdtout?: Function;
stderr?: Function;
exit?: Function;
error?: Function;
}
class ChildProcessStub {
public get stdout(): stream.Readable {
return undefined;
}
public get stderr(): stream.Readable {
return undefined;
}
// tslint:disable-next-line:typedef
on(event: any, listener: any) {
throw new Error('Method not implemented.');
}
addListener(event: string, listener: Function): void {
throw new Error('Method not implemented.');
}
removeListener(event: string, listener: Function): void {
throw new Error('Method not implemented.');
}
}

View File

@@ -0,0 +1,123 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import * as should from 'should';
import * as TypeMoq from 'typemoq';
import * as vscode from 'vscode';
import 'mocha';
import { JupyterServerInstanceStub } from '../common';
import { LocalJupyterServerManager, ServerInstanceFactory } from '../../jupyter/jupyterServerManager';
import JupyterServerInstallation from '../../jupyter/jupyterServerInstallation';
import { Deferred } from '../../common/promise';
import { ApiWrapper } from '../../common/apiWrapper';
import * as testUtils from '../common/testUtils';
import { IServerInstance } from '../../jupyter/common';
import { MockExtensionContext } from '../common/stubs';
describe('Local Jupyter Server Manager', function (): void {
let expectedPath = 'my/notebook.ipynb';
let serverManager: LocalJupyterServerManager;
let deferredInstall: Deferred<JupyterServerInstallation>;
let mockApiWrapper: TypeMoq.IMock<ApiWrapper>;
let mockExtensionContext: MockExtensionContext;
let mockFactory: TypeMoq.IMock<ServerInstanceFactory>;
beforeEach(() => {
mockExtensionContext = new MockExtensionContext();
mockApiWrapper = TypeMoq.Mock.ofType(ApiWrapper);
mockApiWrapper.setup(a => a.showErrorMessage(TypeMoq.It.isAny()));
mockApiWrapper.setup(a => a.getWorkspacePathFromUri(TypeMoq.It.isAny())).returns(() => undefined);
mockFactory = TypeMoq.Mock.ofType(ServerInstanceFactory);
deferredInstall = new Deferred<JupyterServerInstallation>();
serverManager = new LocalJupyterServerManager({
documentPath: expectedPath,
jupyterInstallation: deferredInstall.promise,
extensionContext: mockExtensionContext,
apiWrapper: mockApiWrapper.object,
factory: mockFactory.object
});
});
it('Should not be started initially', function (): void {
should(serverManager.isStarted).be.false();
should(serverManager.serverSettings).be.undefined();
});
it('Should show error message on install failure', async function (): Promise<void> {
let error = 'Error!!';
deferredInstall.reject(error);
await testUtils.assertThrowsAsync(() => serverManager.startServer(), undefined);
mockApiWrapper.verify(a => a.showErrorMessage(TypeMoq.It.isAny()), TypeMoq.Times.once());
});
it('Should configure and start install', async function (): Promise<void> {
// Given an install and instance that start with no issues
let expectedUri = vscode.Uri.parse('http://localhost:1234?token=abcdefghijk');
let [mockInstall, mockServerInstance] = initInstallAndInstance(expectedUri);
deferredInstall.resolve(mockInstall.object);
// When I start the server
let notified = false;
serverManager.onServerStarted(() => notified = true);
await serverManager.startServer();
// Then I expect the port to be included in settings
should(serverManager.serverSettings.baseUrl.indexOf('1234') > -1).be.true();
should(serverManager.serverSettings.token).equal('abcdefghijk');
// And a notification to be sent
should(notified).be.true();
// And the key methods to have been called
mockServerInstance.verify(s => s.configure(), TypeMoq.Times.once());
mockServerInstance.verify(s => s.start(), TypeMoq.Times.once());
});
it('Should not fail on stop if never started', async function (): Promise<void> {
await serverManager.stopServer();
});
it('Should call stop on server instance', async function (): Promise<void> {
// Given an install and instance that start with no issues
let expectedUri = vscode.Uri.parse('http://localhost:1234?token=abcdefghijk');
let [mockInstall, mockServerInstance] = initInstallAndInstance(expectedUri);
mockServerInstance.setup(s => s.stop()).returns(() => Promise.resolve());
deferredInstall.resolve(mockInstall.object);
// When I start and then the server
await serverManager.startServer();
await serverManager.stopServer();
// Then I expect stop to have been called on the server instance
mockServerInstance.verify(s => s.stop(), TypeMoq.Times.once());
});
it('Should call stop when extension is disposed', async function (): Promise<void> {
// Given an install and instance that start with no issues
let expectedUri = vscode.Uri.parse('http://localhost:1234?token=abcdefghijk');
let [mockInstall, mockServerInstance] = initInstallAndInstance(expectedUri);
mockServerInstance.setup(s => s.stop()).returns(() => Promise.resolve());
deferredInstall.resolve(mockInstall.object);
// When I start and then dispose the extension
await serverManager.startServer();
should(mockExtensionContext.subscriptions).have.length(1);
mockExtensionContext.subscriptions[0].dispose();
// Then I expect stop to have been called on the server instance
mockServerInstance.verify(s => s.stop(), TypeMoq.Times.once());
});
function initInstallAndInstance(uri: vscode.Uri): [TypeMoq.IMock<JupyterServerInstallation>, TypeMoq.IMock<IServerInstance>] {
let mockInstall = TypeMoq.Mock.ofType(JupyterServerInstallation, undefined, undefined, '/root');
let mockServerInstance = TypeMoq.Mock.ofType(JupyterServerInstanceStub);
mockFactory.setup(f => f.createInstance(TypeMoq.It.isAny())).returns(() => mockServerInstance.object);
mockServerInstance.setup(s => s.configure()).returns(() => Promise.resolve());
mockServerInstance.setup(s => s.start()).returns(() => Promise.resolve());
mockServerInstance.setup(s => s.uri).returns(() => uri);
return [mockInstall, mockServerInstance];
}
});

View File

@@ -0,0 +1,171 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as should from 'should';
import * as TypeMoq from 'typemoq';
import { nb } from 'sqlops';
import { SessionManager, Session, Kernel } from '@jupyterlab/services';
import 'mocha';
import { JupyterSessionManager, JupyterSession } from '../../jupyter/jupyterSessionManager';
import { Deferred } from '../../common/promise';
import { SessionStub, KernelStub } from '../common';
describe('Jupyter Session Manager', function (): void {
let mockJupyterManager = TypeMoq.Mock.ofType<SessionManager>();
let sessionManager = new JupyterSessionManager();
it('isReady should only be true after ready promise completes', function (done): void {
// Given
let deferred = new Deferred<void>();
mockJupyterManager.setup(m => m.ready).returns(() => deferred.promise);
// When I call before resolve I expect it'll be false
sessionManager.setJupyterSessionManager(mockJupyterManager.object);
should(sessionManager.isReady).be.false();
// When I call a after resolve, it'll be true
deferred.resolve();
sessionManager.ready.then(() => {
should(sessionManager.isReady).be.true();
done();
});
});
it('should passthrough the ready calls', function (done): void {
// Given
let deferred = new Deferred<void>();
mockJupyterManager.setup(m => m.ready).returns(() => deferred.promise);
// When I wait on the ready method before completing
sessionManager.setJupyterSessionManager(mockJupyterManager.object);
sessionManager.ready.then(() => done());
// Then session manager should eventually resolve
deferred.resolve();
});
it('should handle null specs', function (): void {
mockJupyterManager.setup(m => m.specs).returns(() => undefined);
let specs = sessionManager.specs;
should(specs).be.undefined();
});
it('should map specs to named kernels', function (): void {
let internalSpecs: Kernel.ISpecModels = {
default: 'mssql',
kernelspecs: {
'mssql': <Kernel.ISpecModel>{ language: 'sql' },
'python': <Kernel.ISpecModel>{ language: 'python' }
}
};
mockJupyterManager.setup(m => m.specs).returns(() => internalSpecs);
let specs = sessionManager.specs;
should(specs.defaultKernel).equal('mssql');
should(specs.kernels).have.length(2);
});
it('Should call to startSession with correct params', async function (): Promise<void> {
// Given a session request that will complete OK
let sessionOptions: nb.ISessionOptions = { path: 'mypath.ipynb' };
let expectedSessionInfo = <Session.ISession>{
path: sessionOptions.path,
id: 'id',
name: 'sessionName',
type: 'type',
kernel: {
name: 'name'
}
};
mockJupyterManager.setup(m => m.startNew(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedSessionInfo));
// When I call startSession
let session = await sessionManager.startNew(sessionOptions);
// Then I expect the parameters passed to be correct
should(session.path).equal(sessionOptions.path);
should(session.canChangeKernels).be.true();
should(session.id).equal(expectedSessionInfo.id);
should(session.name).equal(expectedSessionInfo.name);
should(session.type).equal(expectedSessionInfo.type);
should(session.kernel.name).equal(expectedSessionInfo.kernel.name);
});
it('Should call to shutdown with correct id', async function (): Promise<void> {
let id = 'session1';
mockJupyterManager.setup(m => m.shutdown(TypeMoq.It.isValue(id))).returns(() => Promise.resolve());
mockJupyterManager.setup(m => m.isDisposed).returns(() => false);
await sessionManager.shutdown(id);
mockJupyterManager.verify(m => m.shutdown(TypeMoq.It.isValue(id)), TypeMoq.Times.once());
});
});
describe('Jupyter Session', function (): void {
let mockJupyterSession: TypeMoq.IMock<SessionStub>;
let session: JupyterSession;
beforeEach(() => {
mockJupyterSession = TypeMoq.Mock.ofType(SessionStub);
session = new JupyterSession(mockJupyterSession.object);
});
it('should always be able to change kernels', function (): void {
should(session.canChangeKernels).be.true();
});
it('should pass through most properties', function (): void {
// Given values for the passthrough properties
mockJupyterSession.setup(s => s.id).returns(() => 'id');
mockJupyterSession.setup(s => s.name).returns(() => 'name');
mockJupyterSession.setup(s => s.path).returns(() => 'path');
mockJupyterSession.setup(s => s.type).returns(() => 'type');
mockJupyterSession.setup(s => s.status).returns(() => 'starting');
// Should return those values when called
should(session.id).equal('id');
should(session.name).equal('name');
should(session.path).equal('path');
should(session.type).equal('type');
should(session.status).equal('starting');
});
it('should handle null kernel', function (): void {
mockJupyterSession.setup(s => s.kernel).returns(() => undefined);
should(session.kernel).be.undefined();
});
it('should passthrough kernel', function (): void {
// Given a kernel with an ID
let kernelMock = TypeMoq.Mock.ofType(KernelStub);
kernelMock.setup(k => k.id).returns(() => 'id');
mockJupyterSession.setup(s => s.kernel).returns(() => kernelMock.object);
// When I get a wrapper for the kernel
let kernel = session.kernel;
kernel = session.kernel;
// Then I expect it to have the ID, and only be called once
should(kernel.id).equal('id');
mockJupyterSession.verify(s => s.kernel, TypeMoq.Times.once());
});
it('should send name in changeKernel request', async function (): Promise<void> {
// Given change kernel returns something
let kernelMock = TypeMoq.Mock.ofType(KernelStub);
kernelMock.setup(k => k.id).returns(() => 'id');
let options: Partial<Kernel.IModel>;
mockJupyterSession.setup(s => s.changeKernel(TypeMoq.It.isAny())).returns((opts) => {
options = opts;
return Promise.resolve(kernelMock.object);
});
// When I call changeKernel on the wrapper
let kernel = await session.changeKernel({
name: 'python'
});
// Then I expect it to have the ID, and only be called once
should(kernel.id).equal('id');
should(options.name).equal('python');
});
});