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

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

View File

@@ -17,10 +17,12 @@ expressly granted herein, whether by implication, estoppel or otherwise.
chokidar: https://github.com/paulmillr/chokidar
comment-json: https://github.com/kaelzhang/node-comment-json
core-js: https://github.com/zloirock/core-js
decompress: https://github.com/kevva/decompress
emmet: https://github.com/emmetio/emmet
error-ex: https://github.com/Qix-/node-error-ex
escape-string-regexp: https://github.com/sindresorhus/escape-string-regexp
fast-plist: https://github.com/Microsoft/node-fast-plist
figures: https://github.com/sindresorhus/figures
find-remove: https://www.npmjs.com/package/find-remove
fs-extra: https://github.com/jprichardson/node-fs-extra
gc-signals: https://github.com/Microsoft/node-gc-signals
@@ -41,10 +43,12 @@ expressly granted herein, whether by implication, estoppel or otherwise.
native-keymap: https://github.com/Microsoft/node-native-keymap
native-watchdog: https://github.com/Microsoft/node-native-watchdog
ng2-charts: https://github.com/valor-software/ng2-charts
node-fetch: https://github.com/bitinn/node-fetch
node-pty: https://github.com/Tyriar/node-pty
nsfw: https://github.com/Axosoft/nsfw
pretty-data: https://github.com/vkiryukhin/pretty-data
primeng: https://github.com/primefaces/primeng
process-nextick-args: https://github.com/calvinmetcalf/process-nextick-args
pty.js: https://github.com/chjj/pty.js
reflect-metadata: https://github.com/rbuckton/reflect-metadata
rxjs: https://github.com/ReactiveX/RxJS
@@ -53,10 +57,13 @@ expressly granted herein, whether by implication, estoppel or otherwise.
sqltoolsservice: https://github.com/Microsoft/sqltoolsservice
svg.js: https://github.com/svgdotjs/svg.js
systemjs: https://github.com/systemjs/systemjs
temp-write: https://github.com/sindresorhus/temp-write
underscore: https://github.com/jashkenas/underscore
v8-profiler: https://github.com/node-inspector/v8-profiler
vscode: https://github.com/microsoft/vscode
vscode-debugprotocol: https://github.com/Microsoft/vscode-debugadapter-node
vscode-languageclient: https://github.com/Microsoft/vscode-languageserver-node
vscode-nls: https://github.com/Microsoft/vscode-nls
vscode-ripgrep: https://github.com/roblourens/vscode-ripgrep
vscode-textmate: https://github.com/Microsoft/vscode-textmate
winreg: https://github.com/fresc81/node-winreg
@@ -64,6 +71,7 @@ expressly granted herein, whether by implication, estoppel or otherwise.
yauzl: https://github.com/thejoshwolfe/yauzl
zone.js: https://www.npmjs.com/package/zone
Microsoft PROSE SDK: https://microsoft.github.io/prose
%% angular NOTICES AND INFORMATION BEGIN HERE
=========================================
@@ -293,6 +301,20 @@ THE SOFTWARE.
=========================================
END OF core-js NOTICES AND INFORMATION
%% decompress NOTICES AND INFORMATION BEGIN HERE
=========================================
MIT License
Copyright (c) Kevin Mårtensson <kevinmartensson@gmail.com> (github.com/kevva)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
=========================================
END OF decompress NOTICES AND INFORMATION
%% emmet NOTICES AND INFORMATION BEGIN HERE
=========================================
The MIT License (MIT)
@@ -394,6 +416,20 @@ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEAL
=========================================
END OF fast-plist NOTICES AND INFORMATION
%% figures NOTICES AND INFORMATION BEGIN HERE
=========================================
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
=========================================
END OF figures NOTICES AND INFORMATION
%% fs-extra NOTICES AND INFORMATION BEGIN HERE
=========================================
(The MIT License)
@@ -1335,6 +1371,32 @@ SOFTWARE.
=========================================
END OF ng2-charts NOTICES AND INFORMATION
%% node-fetch NOTICES AND INFORMATION BEGIN HERE
=========================================
The MIT License (MIT)
Copyright (c) 2016 David Frank
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
=========================================
END OF node-fetch NOTICES AND INFORMATION
%% node-pty NOTICES AND INFORMATION BEGIN HERE
=========================================
Copyright (c) 2012-2015, Christopher Jeffrey (https://github.com/chjj/)
@@ -1409,6 +1471,30 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
=========================================
END OF primeng NOTICES AND INFORMATION
%% process-nextick-args NOTICES AND INFORMATION BEGIN HERE
=========================================
# Copyright (c) 2015 Calvin Metcalf
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.**
=========================================
END OF process-nextick-args NOTICES AND INFORMATION
%% pty.js NOTICES AND INFORMATION BEGIN HERE
=========================================
Copyright (c) 2012-2015, Christopher Jeffrey (https://github.com/chjj/)
@@ -1818,6 +1904,20 @@ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEAL
=========================================
END OF systemjs NOTICES AND INFORMATION
%% temp-write NOTICES AND INFORMATION BEGIN HERE
=========================================
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
=========================================
END OF temp-write NOTICES AND INFORMATION
%% underscore NOTICES AND INFORMATION BEGIN HERE
=========================================
Copyright (c) 2009-2017 Jeremy Ashkenas, DocumentCloud and Investigative
@@ -1920,6 +2020,50 @@ OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWA
=========================================
END OF vscode-debugprotocol NOTICES AND INFORMATION
%% vscode-languageclient NOTICES AND INFORMATION BEGIN HERE
=========================================
Copyright (c) Microsoft Corporation
All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
=========================================
END OF vscode-languageclient NOTICES AND INFORMATION
%% vscode-nls NOTICES AND INFORMATION BEGIN HERE
=========================================
The MIT License (MIT)
Copyright (c) Microsoft Corporation
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
=========================================
END OF vscode-nls NOTICES AND INFORMATION
%% vscode-ripgrep NOTICES AND INFORMATION BEGIN HERE
=========================================
vscode-ripgrep
@@ -2079,3 +2223,188 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
=========================================
END OF zone.js NOTICES AND INFORMATION
%% Microsoft.ProgramSynthesis.Common NOTICES AND INFORMATION BEGIN HERE
=========================================
NOTICES AND INFORMATION
Do Not Translate or Localize
This software incorporates material from third parties. Microsoft makes certain
open source code available at http://3rdpartysource.microsoft.com, or you may
send a check or money order for US $5.00, including the product name, the open
source component name, and version number, to:
Source Code Compliance Team
Microsoft Corporation
One Microsoft Way
Redmond, WA 98052
USA
Notwithstanding any other terms, you may reverse engineer this software to the
extent required to debug changes to any libraries licensed under the GNU Lesser
General Public License.
-------------------------------START OF THIRD-PARTY NOTICES-------------------------------------------
===================================CoreFx (BEGIN)
The MIT License (MIT)
Copyright (c) .NET Foundation and Contributors
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
===================================CoreFx (END)
===================================CoreFxLab (BEGIN)
The MIT License (MIT)
Copyright (c) Microsoft Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
===================================CoreFxLab (END)
===================================Reactive Extensions (BEGIN)
Copyright (c) .NET Foundation and Contributors
All Rights Reserved
Licensed under the Apache License, Version 2.0 (the "License"); you
may not use this file except in compliance with the License. You may
obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied. See the License for the specific language governing permissions
and limitations under the License.
List of contributors to the Rx libraries
Rx and Ix.NET:
Wes Dyer
Jeffrey van Gogh
Matthew Podwysocki
Bart De Smet
Danny van Velzen
Erik Meijer
Brian Beckman
Aaron Lahman
Georgi Chkodrov
Arthur Watson
Gert Drapers
Mark Shields
Eric Rozell
Rx.js and Ix.js:
Matthew Podwysocki
Jeffrey van Gogh
Bart De Smet
Brian Beckman
Wes Dyer
Erik Meijer
Tx:
Georgi Chkodrov
Bart De Smet
Aaron Lahman
Erik Meijer
Brian Grunkemeyer
Beysim Sezgin
Tiho Tarnavski
Collin Meek
Sajay Anthony
Karen Albrecht
John Allen
Zach Kramer
Rx++ and Ix++:
Aaron Lahman
===================================Reactive Extensions (END)
-------------------------------END OF THIRD-PARTY NOTICES-------------------------------------------
=========================================
END OF Microsoft.ProgramSynthesis.Common NOTICES AND INFORMATION
%% Microsoft.ProgramSynthesis.Detection NOTICES AND INFORMATION BEGIN HERE
=========================================
NOTICES AND INFORMATION
Do Not Translate or Localize
This software incorporates material from third parties. Microsoft makes certain
open source code available at http://3rdpartysource.microsoft.com, or you may
send a check or money order for US $5.00, including the product name, the open
source component name, and version number, to:
Source Code Compliance Team
Microsoft Corporation
One Microsoft Way
Redmond, WA 98052
USA
Notwithstanding any other terms, you may reverse engineer this software to the
extent required to debug changes to any libraries licensed under the GNU Lesser
General Public License.
-------------------------------START OF THIRD-PARTY NOTICES-------------------------------------------
===================================ExcelDataReader (BEGIN)
The MIT License (MIT)
Copyright (c) 2014 ExcelDataReader
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
===================================ExcelDataReader (END)
-------------------------------END OF THIRD-PARTY NOTICES-------------------------------------------
=========================================
END OF Microsoft.ProgramSynthesis.Detection NOTICES AND INFORMATION

1
extensions/notebook/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
jupyter_config/**

View File

@@ -0,0 +1 @@
jupyter_config/**

View File

@@ -0,0 +1,10 @@
{
"argv": [
"python",
"-m",
"sparkmagic.kernels.pyspark3kernel.pyspark3kernel",
"-f",
"{connection_file}"
],
"display_name": "PySpark3"
}

View File

@@ -0,0 +1,10 @@
{
"argv": [
"python",
"-m",
"sparkmagic.kernels.pysparkkernel.pysparkkernel",
"-f",
"{connection_file}"
],
"display_name": "PySpark"
}

View File

@@ -0,0 +1,10 @@
{
"argv": [
"python",
"-m",
"sparkmagic.kernels.sparkkernel.sparkkernel",
"-f",
"{connection_file}"
],
"display_name": "Spark | Scala"
}

View File

@@ -0,0 +1,10 @@
{
"argv": [
"python",
"-m",
"sparkmagic.kernels.sparkrkernel.sparkrkernel",
"-f",
"{connection_file}"
],
"display_name": "Spark | R"
}

View File

@@ -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
}

View File

@@ -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"
}

View 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

View File

@@ -0,0 +1,4 @@
// Make sure that all links load in the same tab
define(['base/js/namespace'], function (Jupyter) {
Jupyter._target = '_self';
});

View File

@@ -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

View File

@@ -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
}
}
}
}

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

0
scripts/sql-test-integration.sh Normal file → Executable file
View File

22
scripts/test-extensions.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/bash
set -e
if [[ "$OSTYPE" == "darwin"* ]]; then
realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; }
ROOT=$(dirname $(dirname $(realpath "$0")))
VSCODEUSERDATADIR=`mktemp -d -t 'myuserdatadir'`
VSCODEEXTDIR=`mktemp -d -t 'myextdir'`
else
ROOT=$(dirname $(dirname $(readlink -f $0)))
VSCODEUSERDATADIR=`mktemp -d 2>/dev/null`
VSCODEEXTDIR=`mktemp -d 2>/dev/null`
fi
cd $ROOT
echo $VSCODEUSERDATADIR
echo $VSCODEEXTDIR
./scripts/code.sh --extensionDevelopmentPath=$ROOT/extensions/notebook --extensionTestsPath=$ROOT/extensions/notebook/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR
rm -r $VSCODEUSERDATADIR
rm -r $VSCODEEXTDIR