mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Move SQL 2019 extension's notebook code into Azure Data Studio (#4090)
This commit is contained in:
1
extensions/notebook/.gitignore
vendored
Normal file
1
extensions/notebook/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
jupyter_config/**
|
||||
1
extensions/notebook/.vscodeignore
Normal file
1
extensions/notebook/.vscodeignore
Normal file
@@ -0,0 +1 @@
|
||||
jupyter_config/**
|
||||
10
extensions/notebook/kernels/pyspark3kernel/kernel.json
Normal file
10
extensions/notebook/kernels/pyspark3kernel/kernel.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"argv": [
|
||||
"python",
|
||||
"-m",
|
||||
"sparkmagic.kernels.pyspark3kernel.pyspark3kernel",
|
||||
"-f",
|
||||
"{connection_file}"
|
||||
],
|
||||
"display_name": "PySpark3"
|
||||
}
|
||||
10
extensions/notebook/kernels/pysparkkernel/kernel.json
Normal file
10
extensions/notebook/kernels/pysparkkernel/kernel.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"argv": [
|
||||
"python",
|
||||
"-m",
|
||||
"sparkmagic.kernels.pysparkkernel.pysparkkernel",
|
||||
"-f",
|
||||
"{connection_file}"
|
||||
],
|
||||
"display_name": "PySpark"
|
||||
}
|
||||
10
extensions/notebook/kernels/sparkkernel/kernel.json
Normal file
10
extensions/notebook/kernels/sparkkernel/kernel.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"argv": [
|
||||
"python",
|
||||
"-m",
|
||||
"sparkmagic.kernels.sparkkernel.sparkkernel",
|
||||
"-f",
|
||||
"{connection_file}"
|
||||
],
|
||||
"display_name": "Spark | Scala"
|
||||
}
|
||||
10
extensions/notebook/kernels/sparkrkernel/kernel.json
Normal file
10
extensions/notebook/kernels/sparkrkernel/kernel.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"argv": [
|
||||
"python",
|
||||
"-m",
|
||||
"sparkmagic.kernels.sparkrkernel.sparkrkernel",
|
||||
"-f",
|
||||
"{connection_file}"
|
||||
],
|
||||
"display_name": "Spark | R"
|
||||
}
|
||||
@@ -66,6 +66,63 @@
|
||||
{
|
||||
"command": "notebook.command.addtext",
|
||||
"title": "%notebook.command.addtext%"
|
||||
},
|
||||
{
|
||||
"command": "jupyter.cmd.analyzeNotebook",
|
||||
"title": "%title.analyzeJupyterNotebook%"
|
||||
},
|
||||
{
|
||||
"command": "jupyter.task.newNotebook",
|
||||
"title": "%title.newJupyterNotebook%",
|
||||
"icon": {
|
||||
"dark": "resources/dark/new_notebook_inverse.svg",
|
||||
"light": "resources/light/new_notebook.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "jupyter.task.openNotebook",
|
||||
"title": "%title.openJupyterNotebook%",
|
||||
"icon": {
|
||||
"dark": "resources/dark/open_notebook_inverse.svg",
|
||||
"light": "resources/light/open_notebook.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "jupyter.cmd.newNotebook",
|
||||
"title": "%title.newJupyterNotebook%",
|
||||
"icon": {
|
||||
"dark": "resources/dark/new_notebook_inverse.svg",
|
||||
"light": "resources/light/new_notebook.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "jupyter.cmd.installPackages",
|
||||
"title": "%title.installPackages%",
|
||||
"icon": {
|
||||
"dark": "resources/dark/manage_inverse.svg",
|
||||
"light": "resources/light/manage.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "jupyter.cmd.configurePython",
|
||||
"title": "%title.configurePython%"
|
||||
},
|
||||
{
|
||||
"command": "jupyter.reinstallDependencies",
|
||||
"title": "%title.reinstallNotebookDependencies%"
|
||||
}
|
||||
],
|
||||
"languages": [
|
||||
{
|
||||
"id": "jupyter-notebook",
|
||||
"extensions": [
|
||||
".ipynb"
|
||||
],
|
||||
"aliases": [
|
||||
"Jupyter Notebook",
|
||||
"IPython Notebook",
|
||||
"ipy"
|
||||
]
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
@@ -90,6 +147,26 @@
|
||||
{
|
||||
"command": "notebook.command.addtext",
|
||||
"when": "notebookEditorVisible"
|
||||
},
|
||||
{
|
||||
"command": "jupyter.task.newNotebook",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "jupyter.cmd.newNotebook",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "jupyter.cmd.analyzeNotebook",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "jupyter.task.openNotebook",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "jupyter.cmd.installPackages",
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"objectExplorer/item/context": [
|
||||
@@ -102,6 +179,22 @@
|
||||
"command": "notebook.command.analyzeNotebook",
|
||||
"when": "nodeType=~/^mssqlCluster/ && nodeLabel=~/[^\\s]+(\\.(csv|tsv|txt))$/ && nodeType == mssqlCluster:file",
|
||||
"group": "1notebook@1"
|
||||
},
|
||||
{
|
||||
"command": "jupyter.cmd.newNotebook",
|
||||
"when": "connectionProvider == HADOOP_KNOX && nodeType && nodeType == Server",
|
||||
"group": "1root@1"
|
||||
},
|
||||
{
|
||||
"command": "jupyter.cmd.analyzeNotebook",
|
||||
"when": "nodeType=~/^hdfs/ && nodeLabel=~/[^\\s]+(\\.(csv|tsv|txt))$/ && nodeType == hdfs:file",
|
||||
"group": "1notebook@1"
|
||||
}
|
||||
],
|
||||
"notebook/toolbar": [
|
||||
{
|
||||
"command": "jupyter.cmd.installPackages",
|
||||
"when": "providerId == jupyter"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -145,12 +238,69 @@
|
||||
"executionTarget": null,
|
||||
"kernels": ["sql"]
|
||||
}
|
||||
]
|
||||
],
|
||||
"notebook.providers": {
|
||||
"provider": "jupyter",
|
||||
"fileExtensions": [
|
||||
"IPYNB"
|
||||
],
|
||||
"standardKernels": [
|
||||
{
|
||||
"name": "Python 3",
|
||||
"connectionProviderIds": []
|
||||
},
|
||||
{
|
||||
"name": "PySpark",
|
||||
"connectionProviderIds": [
|
||||
"HADOOP_KNOX",
|
||||
"MSSQL"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PySpark3",
|
||||
"connectionProviderIds": [
|
||||
"HADOOP_KNOX",
|
||||
"MSSQL"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Spark | R",
|
||||
"connectionProviderIds": [
|
||||
"HADOOP_KNOX",
|
||||
"MSSQL"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Spark | Scala",
|
||||
"connectionProviderIds": [
|
||||
"HADOOP_KNOX",
|
||||
"MSSQL"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-nls": "^4.0.0"
|
||||
"@jupyterlab/services": "^3.2.1",
|
||||
"decompress": "^4.2.0",
|
||||
"error-ex": "^1.3.1",
|
||||
"figures": "^2.0.0",
|
||||
"fs-extra": "^5.0.0",
|
||||
"node-fetch": "^2.3.0",
|
||||
"process-nextick-args": "^2.0.0",
|
||||
"temp-write": "^3.4.0",
|
||||
"vscode-languageclient": "^5.3.0-next.1",
|
||||
"vscode-nls": "2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "8.0.33"
|
||||
}
|
||||
}
|
||||
"@types/mocha": "^5.2.5",
|
||||
"@types/node": "^11.9.3",
|
||||
"assert": "^1.4.1",
|
||||
"mocha": "^5.2.0",
|
||||
"mocha-junit-reporter": "^1.17.0",
|
||||
"mocha-multi-reporters": "^1.1.7",
|
||||
"typemoq": "^2.1.0",
|
||||
"vscode": "1.1.5"
|
||||
},
|
||||
"enableProposedApi": true
|
||||
}
|
||||
|
||||
@@ -10,5 +10,16 @@
|
||||
"notebook.analyzeJupyterNotebook": "Analyze in Notebook",
|
||||
"notebook.command.runactivecell": "Run Cell",
|
||||
"notebook.command.addcode": "Add Code Cell",
|
||||
"notebook.command.addtext": "Add Text Cell"
|
||||
"notebook.command.addtext": "Add Text Cell",
|
||||
"title.analyzeJupyterNotebook": "Analyze in Notebook",
|
||||
"title.newJupyterNotebook": "New Notebook",
|
||||
"title.openJupyterNotebook": "Open Notebook",
|
||||
"title.jupyter.setContext": "Set context for Notebook",
|
||||
"title.jupyter.setKernel": "Set kernel for Notebook",
|
||||
"config.jupyter.extraKernelsTitle": "Extra kernels",
|
||||
"config.jupyter.extraKernelsDescription": "IDs of the extra kernels to enable",
|
||||
"config.jupyter.kernelConfigValuesDescription": "Configuration options for Jupyter kernels. This is automatically managed and not recommended to be manually edited.",
|
||||
"title.reinstallNotebookDependencies": "Reinstall Notebook dependencies",
|
||||
"title.configurePython": "Configure Python for Notebooks",
|
||||
"title.installPackages": "Install Packages"
|
||||
}
|
||||
1
extensions/notebook/resources/dark/manage_inverse.svg
Normal file
1
extensions/notebook/resources/dark/manage_inverse.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>configure_inverse</title><path class="cls-1" d="M15.08,1.71c.14.23.26.46.37.67a5.88,5.88,0,0,1,.29.65,4,4,0,0,1,.19.68,4.27,4.27,0,0,1,.07.78,4.43,4.43,0,0,1-.16,1.19,4.51,4.51,0,0,1-3.15,3.15A4.43,4.43,0,0,1,11.5,9l-.36,0-.36,0-6.3,6.3a2.56,2.56,0,0,1-.86.57,2.65,2.65,0,0,1-1,.2,2.53,2.53,0,0,1-1-.21A2.65,2.65,0,0,1,.21,14.39a2.53,2.53,0,0,1-.21-1,2.65,2.65,0,0,1,.2-1,2.56,2.56,0,0,1,.57-.86l6.3-6.3Q7,5,7,4.86c0-.12,0-.24,0-.36A4.43,4.43,0,0,1,7.16,3.3,4.51,4.51,0,0,1,10.31.15,4.43,4.43,0,0,1,11.5,0a4.27,4.27,0,0,1,.78.07A4,4,0,0,1,13,.25a5.88,5.88,0,0,1,.65.29l.67.37L11.2,4l.8.8ZM11.5,8a3.38,3.38,0,0,0,1.36-.28,3.53,3.53,0,0,0,1.86-1.86A3.38,3.38,0,0,0,15,4.49a3.29,3.29,0,0,0-.19-1.1L12,6.19,9.8,4l2.8-2.81A3.29,3.29,0,0,0,11.5,1a3.38,3.38,0,0,0-1.36.28A3.53,3.53,0,0,0,8.28,3.13,3.38,3.38,0,0,0,8,4.49,3,3,0,0,0,8,5q0,.26.11.52L1.48,12.23A1.62,1.62,0,0,0,1,13.37a1.55,1.55,0,0,0,.13.63,1.63,1.63,0,0,0,.86.86,1.55,1.55,0,0,0,.63.13,1.62,1.62,0,0,0,1.15-.48l6.69-6.68.52.11A3,3,0,0,0,11.5,8Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
4
extensions/notebook/resources/jupyter_config/custom.js
Normal file
4
extensions/notebook/resources/jupyter_config/custom.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// Make sure that all links load in the same tab
|
||||
define(['base/js/namespace'], function (Jupyter) {
|
||||
Jupyter._target = '_self';
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
# Disable CSP in order to load Jupyter inside Azure Data Studio
|
||||
c.NotebookApp.tornado_settings = {
|
||||
'headers': {'Content-Security-Policy': ''}
|
||||
}
|
||||
|
||||
c.NotebookApp.open_browser = False
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"kernel_python_credentials": {
|
||||
"url": "",
|
||||
"auth": "None"
|
||||
},
|
||||
"kernel_scala_credentials": {
|
||||
"url": "",
|
||||
"auth": "None"
|
||||
},
|
||||
"kernel_r_credentials": {
|
||||
"url": "",
|
||||
"auth": "None"
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
extensions/notebook/resources/light/manage.svg
Normal file
1
extensions/notebook/resources/light/manage.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>configure</title><path d="M15.08,1.71c.14.23.26.46.37.67a5.88,5.88,0,0,1,.29.65,4,4,0,0,1,.19.68,4.27,4.27,0,0,1,.07.78,4.43,4.43,0,0,1-.16,1.19,4.51,4.51,0,0,1-3.15,3.15A4.43,4.43,0,0,1,11.5,9l-.36,0-.36,0-6.3,6.3a2.56,2.56,0,0,1-.86.57,2.65,2.65,0,0,1-1,.2,2.53,2.53,0,0,1-1-.21A2.65,2.65,0,0,1,.21,14.39a2.53,2.53,0,0,1-.21-1,2.65,2.65,0,0,1,.2-1,2.56,2.56,0,0,1,.57-.86l6.3-6.3Q7,5,7,4.86c0-.12,0-.24,0-.36A4.43,4.43,0,0,1,7.16,3.3,4.51,4.51,0,0,1,10.31.15,4.43,4.43,0,0,1,11.5,0a4.27,4.27,0,0,1,.78.07A4,4,0,0,1,13,.25a5.88,5.88,0,0,1,.65.29l.67.37L11.2,4l.8.8ZM11.5,8a3.38,3.38,0,0,0,1.36-.28,3.53,3.53,0,0,0,1.86-1.86A3.38,3.38,0,0,0,15,4.49a3.29,3.29,0,0,0-.19-1.1L12,6.19,9.8,4l2.8-2.81A3.29,3.29,0,0,0,11.5,1a3.38,3.38,0,0,0-1.36.28A3.53,3.53,0,0,0,8.28,3.13,3.38,3.38,0,0,0,8,4.49,3,3,0,0,0,8,5q0,.26.11.52L1.48,12.23A1.62,1.62,0,0,0,1,13.37a1.55,1.55,0,0,0,.13.63,1.63,1.63,0,0,0,.86.86,1.55,1.55,0,0,0,.63.13,1.62,1.62,0,0,0,1.15-.48l6.69-6.68.52.11A3,3,0,0,0,11.5,8Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
82
extensions/notebook/src/common/apiWrapper.ts
Normal file
82
extensions/notebook/src/common/apiWrapper.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
28
extensions/notebook/src/common/appContext.ts
Normal file
28
extensions/notebook/src/common/appContext.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
48
extensions/notebook/src/common/constants.ts
Normal file
48
extensions/notebook/src/common/constants.ts
Normal 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'
|
||||
}
|
||||
19
extensions/notebook/src/common/localizedConstants.ts
Normal file
19
extensions/notebook/src/common/localizedConstants.ts
Normal 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.');
|
||||
24
extensions/notebook/src/common/notebookUtils.ts
Normal file
24
extensions/notebook/src/common/notebookUtils.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
115
extensions/notebook/src/common/ports.ts
Normal file
115
extensions/notebook/src/common/ports.ts
Normal 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
|
||||
}
|
||||
}
|
||||
26
extensions/notebook/src/common/promise.ts
Normal file
26
extensions/notebook/src/common/promise.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
126
extensions/notebook/src/common/utils.ts
Normal file
126
extensions/notebook/src/common/utils.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
66
extensions/notebook/src/contracts/content.ts
Normal file
66
extensions/notebook/src/contracts/content.ts
Normal 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';
|
||||
171
extensions/notebook/src/dialog/configurePythonDialog.ts
Normal file
171
extensions/notebook/src/dialog/configurePythonDialog.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
200
extensions/notebook/src/intellisense/completionItemProvider.ts
Normal file
200
extensions/notebook/src/intellisense/completionItemProvider.ts
Normal 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;
|
||||
}
|
||||
72
extensions/notebook/src/intellisense/text.ts
Normal file
72
extensions/notebook/src/intellisense/text.ts
Normal 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;
|
||||
}
|
||||
16
extensions/notebook/src/jupyter/common.ts
Normal file
16
extensions/notebook/src/jupyter/common.ts
Normal 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>;
|
||||
}
|
||||
260
extensions/notebook/src/jupyter/jupyterController.ts
Normal file
260
extensions/notebook/src/jupyter/jupyterController.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
172
extensions/notebook/src/jupyter/jupyterKernel.ts
Normal file
172
extensions/notebook/src/jupyter/jupyterKernel.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
56
extensions/notebook/src/jupyter/jupyterNotebookManager.ts
Normal file
56
extensions/notebook/src/jupyter/jupyterNotebookManager.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
82
extensions/notebook/src/jupyter/jupyterNotebookProvider.ts
Normal file
82
extensions/notebook/src/jupyter/jupyterNotebookProvider.ts
Normal 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"]
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
338
extensions/notebook/src/jupyter/jupyterServerInstallation.ts
Normal file
338
extensions/notebook/src/jupyter/jupyterServerInstallation.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
147
extensions/notebook/src/jupyter/jupyterServerManager.ts
Normal file
147
extensions/notebook/src/jupyter/jupyterServerManager.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
338
extensions/notebook/src/jupyter/jupyterSessionManager.ts
Normal file
338
extensions/notebook/src/jupyter/jupyterSessionManager.ts
Normal 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
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
76
extensions/notebook/src/jupyter/jupyterSettingWriter.ts
Normal file
76
extensions/notebook/src/jupyter/jupyterSettingWriter.ts
Normal 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}`;
|
||||
}
|
||||
}
|
||||
43
extensions/notebook/src/jupyter/remoteContentManager.ts
Normal file
43
extensions/notebook/src/jupyter/remoteContentManager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
399
extensions/notebook/src/jupyter/serverInstance.ts
Normal file
399
extensions/notebook/src/jupyter/serverInstance.ts
Normal 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
|
||||
}
|
||||
173
extensions/notebook/src/prompts/adapter.ts
Normal file
173
extensions/notebook/src/prompts/adapter.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
52
extensions/notebook/src/prompts/checkbox.ts
Normal file
52
extensions/notebook/src/prompts/checkbox.ts
Normal 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;
|
||||
}, []);
|
||||
});
|
||||
}
|
||||
}
|
||||
34
extensions/notebook/src/prompts/confirm.ts
Normal file
34
extensions/notebook/src/prompts/confirm.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
3
extensions/notebook/src/prompts/escapeException.ts
Normal file
3
extensions/notebook/src/prompts/escapeException.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
export default require('error-ex')('EscapeException');
|
||||
78
extensions/notebook/src/prompts/expand.ts
Normal file
78
extensions/notebook/src/prompts/expand.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
39
extensions/notebook/src/prompts/factory.ts
Normal file
39
extensions/notebook/src/prompts/factory.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
59
extensions/notebook/src/prompts/input.ts
Normal file
59
extensions/notebook/src/prompts/input.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
34
extensions/notebook/src/prompts/list.ts
Normal file
34
extensions/notebook/src/prompts/list.ts
Normal 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];
|
||||
});
|
||||
}
|
||||
}
|
||||
14
extensions/notebook/src/prompts/password.ts
Normal file
14
extensions/notebook/src/prompts/password.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
70
extensions/notebook/src/prompts/progressIndicator.ts
Normal file
70
extensions/notebook/src/prompts/progressIndicator.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
extensions/notebook/src/prompts/prompt.ts
Normal file
33
extensions/notebook/src/prompts/prompt.ts
Normal 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;
|
||||
68
extensions/notebook/src/prompts/question.ts
Normal file
68
extensions/notebook/src/prompts/question.ts
Normal 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;
|
||||
}
|
||||
259
extensions/notebook/src/test/common.ts
Normal file
259
extensions/notebook/src/test/common.ts
Normal 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
|
||||
66
extensions/notebook/src/test/common/port.test.ts
Normal file
66
extensions/notebook/src/test/common/port.test.ts
Normal 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));
|
||||
});
|
||||
});
|
||||
|
||||
24
extensions/notebook/src/test/common/querybookUtils.test.ts
Normal file
24
extensions/notebook/src/test/common/querybookUtils.test.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
46
extensions/notebook/src/test/common/stubs.ts
Normal file
46
extensions/notebook/src/test/common/stubs.ts
Normal 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.');
|
||||
}
|
||||
}
|
||||
22
extensions/notebook/src/test/common/testUtils.ts
Normal file
22
extensions/notebook/src/test/common/testUtils.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
32
extensions/notebook/src/test/index.ts
Normal file
32
extensions/notebook/src/test/index.ts
Normal 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;
|
||||
97
extensions/notebook/src/test/model/contentManagers.test.ts
Normal file
97
extensions/notebook/src/test/model/contentManagers.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
203
extensions/notebook/src/test/model/kernel.test.ts
Normal file
203
extensions/notebook/src/test/model/kernel.test.ts
Normal 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');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
236
extensions/notebook/src/test/model/serverInstance.test.ts
Normal file
236
extensions/notebook/src/test/model/serverInstance.test.ts
Normal 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.');
|
||||
}
|
||||
}
|
||||
123
extensions/notebook/src/test/model/serverManager.test.ts
Normal file
123
extensions/notebook/src/test/model/serverManager.test.ts
Normal 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];
|
||||
}
|
||||
});
|
||||
171
extensions/notebook/src/test/model/sessionManager.test.ts
Normal file
171
extensions/notebook/src/test/model/sessionManager.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@
|
||||
"target": "es6",
|
||||
"outDir": "./out",
|
||||
"lib": [
|
||||
"es6", "es2015.promise"
|
||||
"dom", "es6", "es2015.promise"
|
||||
],
|
||||
"typeRoots": [
|
||||
"./node_modules/@types"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user