mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-13 17:22:15 -05:00
Move SQL 2019 extension's notebook code into Azure Data Studio (#4090)
This commit is contained in:
@@ -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
1
extensions/notebook/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
jupyter_config/**
|
||||
1
extensions/notebook/.vscodeignore
Normal file
1
extensions/notebook/.vscodeignore
Normal file
@@ -0,0 +1 @@
|
||||
jupyter_config/**
|
||||
10
extensions/notebook/kernels/pyspark3kernel/kernel.json
Normal file
10
extensions/notebook/kernels/pyspark3kernel/kernel.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"argv": [
|
||||
"python",
|
||||
"-m",
|
||||
"sparkmagic.kernels.pyspark3kernel.pyspark3kernel",
|
||||
"-f",
|
||||
"{connection_file}"
|
||||
],
|
||||
"display_name": "PySpark3"
|
||||
}
|
||||
10
extensions/notebook/kernels/pysparkkernel/kernel.json
Normal file
10
extensions/notebook/kernels/pysparkkernel/kernel.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"argv": [
|
||||
"python",
|
||||
"-m",
|
||||
"sparkmagic.kernels.pysparkkernel.pysparkkernel",
|
||||
"-f",
|
||||
"{connection_file}"
|
||||
],
|
||||
"display_name": "PySpark"
|
||||
}
|
||||
10
extensions/notebook/kernels/sparkkernel/kernel.json
Normal file
10
extensions/notebook/kernels/sparkkernel/kernel.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"argv": [
|
||||
"python",
|
||||
"-m",
|
||||
"sparkmagic.kernels.sparkkernel.sparkkernel",
|
||||
"-f",
|
||||
"{connection_file}"
|
||||
],
|
||||
"display_name": "Spark | Scala"
|
||||
}
|
||||
10
extensions/notebook/kernels/sparkrkernel/kernel.json
Normal file
10
extensions/notebook/kernels/sparkrkernel/kernel.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"argv": [
|
||||
"python",
|
||||
"-m",
|
||||
"sparkmagic.kernels.sparkrkernel.sparkrkernel",
|
||||
"-f",
|
||||
"{connection_file}"
|
||||
],
|
||||
"display_name": "Spark | R"
|
||||
}
|
||||
@@ -66,6 +66,63 @@
|
||||
{
|
||||
"command": "notebook.command.addtext",
|
||||
"title": "%notebook.command.addtext%"
|
||||
},
|
||||
{
|
||||
"command": "jupyter.cmd.analyzeNotebook",
|
||||
"title": "%title.analyzeJupyterNotebook%"
|
||||
},
|
||||
{
|
||||
"command": "jupyter.task.newNotebook",
|
||||
"title": "%title.newJupyterNotebook%",
|
||||
"icon": {
|
||||
"dark": "resources/dark/new_notebook_inverse.svg",
|
||||
"light": "resources/light/new_notebook.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "jupyter.task.openNotebook",
|
||||
"title": "%title.openJupyterNotebook%",
|
||||
"icon": {
|
||||
"dark": "resources/dark/open_notebook_inverse.svg",
|
||||
"light": "resources/light/open_notebook.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "jupyter.cmd.newNotebook",
|
||||
"title": "%title.newJupyterNotebook%",
|
||||
"icon": {
|
||||
"dark": "resources/dark/new_notebook_inverse.svg",
|
||||
"light": "resources/light/new_notebook.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "jupyter.cmd.installPackages",
|
||||
"title": "%title.installPackages%",
|
||||
"icon": {
|
||||
"dark": "resources/dark/manage_inverse.svg",
|
||||
"light": "resources/light/manage.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "jupyter.cmd.configurePython",
|
||||
"title": "%title.configurePython%"
|
||||
},
|
||||
{
|
||||
"command": "jupyter.reinstallDependencies",
|
||||
"title": "%title.reinstallNotebookDependencies%"
|
||||
}
|
||||
],
|
||||
"languages": [
|
||||
{
|
||||
"id": "jupyter-notebook",
|
||||
"extensions": [
|
||||
".ipynb"
|
||||
],
|
||||
"aliases": [
|
||||
"Jupyter Notebook",
|
||||
"IPython Notebook",
|
||||
"ipy"
|
||||
]
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
@@ -90,6 +147,26 @@
|
||||
{
|
||||
"command": "notebook.command.addtext",
|
||||
"when": "notebookEditorVisible"
|
||||
},
|
||||
{
|
||||
"command": "jupyter.task.newNotebook",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "jupyter.cmd.newNotebook",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "jupyter.cmd.analyzeNotebook",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "jupyter.task.openNotebook",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "jupyter.cmd.installPackages",
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"objectExplorer/item/context": [
|
||||
@@ -102,6 +179,22 @@
|
||||
"command": "notebook.command.analyzeNotebook",
|
||||
"when": "nodeType=~/^mssqlCluster/ && nodeLabel=~/[^\\s]+(\\.(csv|tsv|txt))$/ && nodeType == mssqlCluster:file",
|
||||
"group": "1notebook@1"
|
||||
},
|
||||
{
|
||||
"command": "jupyter.cmd.newNotebook",
|
||||
"when": "connectionProvider == HADOOP_KNOX && nodeType && nodeType == Server",
|
||||
"group": "1root@1"
|
||||
},
|
||||
{
|
||||
"command": "jupyter.cmd.analyzeNotebook",
|
||||
"when": "nodeType=~/^hdfs/ && nodeLabel=~/[^\\s]+(\\.(csv|tsv|txt))$/ && nodeType == hdfs:file",
|
||||
"group": "1notebook@1"
|
||||
}
|
||||
],
|
||||
"notebook/toolbar": [
|
||||
{
|
||||
"command": "jupyter.cmd.installPackages",
|
||||
"when": "providerId == jupyter"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -145,12 +238,69 @@
|
||||
"executionTarget": null,
|
||||
"kernels": ["sql"]
|
||||
}
|
||||
]
|
||||
],
|
||||
"notebook.providers": {
|
||||
"provider": "jupyter",
|
||||
"fileExtensions": [
|
||||
"IPYNB"
|
||||
],
|
||||
"standardKernels": [
|
||||
{
|
||||
"name": "Python 3",
|
||||
"connectionProviderIds": []
|
||||
},
|
||||
{
|
||||
"name": "PySpark",
|
||||
"connectionProviderIds": [
|
||||
"HADOOP_KNOX",
|
||||
"MSSQL"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PySpark3",
|
||||
"connectionProviderIds": [
|
||||
"HADOOP_KNOX",
|
||||
"MSSQL"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Spark | R",
|
||||
"connectionProviderIds": [
|
||||
"HADOOP_KNOX",
|
||||
"MSSQL"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Spark | Scala",
|
||||
"connectionProviderIds": [
|
||||
"HADOOP_KNOX",
|
||||
"MSSQL"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-nls": "^4.0.0"
|
||||
"@jupyterlab/services": "^3.2.1",
|
||||
"decompress": "^4.2.0",
|
||||
"error-ex": "^1.3.1",
|
||||
"figures": "^2.0.0",
|
||||
"fs-extra": "^5.0.0",
|
||||
"node-fetch": "^2.3.0",
|
||||
"process-nextick-args": "^2.0.0",
|
||||
"temp-write": "^3.4.0",
|
||||
"vscode-languageclient": "^5.3.0-next.1",
|
||||
"vscode-nls": "2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "8.0.33"
|
||||
}
|
||||
}
|
||||
"@types/mocha": "^5.2.5",
|
||||
"@types/node": "^11.9.3",
|
||||
"assert": "^1.4.1",
|
||||
"mocha": "^5.2.0",
|
||||
"mocha-junit-reporter": "^1.17.0",
|
||||
"mocha-multi-reporters": "^1.1.7",
|
||||
"typemoq": "^2.1.0",
|
||||
"vscode": "1.1.5"
|
||||
},
|
||||
"enableProposedApi": true
|
||||
}
|
||||
|
||||
@@ -10,5 +10,16 @@
|
||||
"notebook.analyzeJupyterNotebook": "Analyze in Notebook",
|
||||
"notebook.command.runactivecell": "Run Cell",
|
||||
"notebook.command.addcode": "Add Code Cell",
|
||||
"notebook.command.addtext": "Add Text Cell"
|
||||
"notebook.command.addtext": "Add Text Cell",
|
||||
"title.analyzeJupyterNotebook": "Analyze in Notebook",
|
||||
"title.newJupyterNotebook": "New Notebook",
|
||||
"title.openJupyterNotebook": "Open Notebook",
|
||||
"title.jupyter.setContext": "Set context for Notebook",
|
||||
"title.jupyter.setKernel": "Set kernel for Notebook",
|
||||
"config.jupyter.extraKernelsTitle": "Extra kernels",
|
||||
"config.jupyter.extraKernelsDescription": "IDs of the extra kernels to enable",
|
||||
"config.jupyter.kernelConfigValuesDescription": "Configuration options for Jupyter kernels. This is automatically managed and not recommended to be manually edited.",
|
||||
"title.reinstallNotebookDependencies": "Reinstall Notebook dependencies",
|
||||
"title.configurePython": "Configure Python for Notebooks",
|
||||
"title.installPackages": "Install Packages"
|
||||
}
|
||||
1
extensions/notebook/resources/dark/manage_inverse.svg
Normal file
1
extensions/notebook/resources/dark/manage_inverse.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>configure_inverse</title><path class="cls-1" d="M15.08,1.71c.14.23.26.46.37.67a5.88,5.88,0,0,1,.29.65,4,4,0,0,1,.19.68,4.27,4.27,0,0,1,.07.78,4.43,4.43,0,0,1-.16,1.19,4.51,4.51,0,0,1-3.15,3.15A4.43,4.43,0,0,1,11.5,9l-.36,0-.36,0-6.3,6.3a2.56,2.56,0,0,1-.86.57,2.65,2.65,0,0,1-1,.2,2.53,2.53,0,0,1-1-.21A2.65,2.65,0,0,1,.21,14.39a2.53,2.53,0,0,1-.21-1,2.65,2.65,0,0,1,.2-1,2.56,2.56,0,0,1,.57-.86l6.3-6.3Q7,5,7,4.86c0-.12,0-.24,0-.36A4.43,4.43,0,0,1,7.16,3.3,4.51,4.51,0,0,1,10.31.15,4.43,4.43,0,0,1,11.5,0a4.27,4.27,0,0,1,.78.07A4,4,0,0,1,13,.25a5.88,5.88,0,0,1,.65.29l.67.37L11.2,4l.8.8ZM11.5,8a3.38,3.38,0,0,0,1.36-.28,3.53,3.53,0,0,0,1.86-1.86A3.38,3.38,0,0,0,15,4.49a3.29,3.29,0,0,0-.19-1.1L12,6.19,9.8,4l2.8-2.81A3.29,3.29,0,0,0,11.5,1a3.38,3.38,0,0,0-1.36.28A3.53,3.53,0,0,0,8.28,3.13,3.38,3.38,0,0,0,8,4.49,3,3,0,0,0,8,5q0,.26.11.52L1.48,12.23A1.62,1.62,0,0,0,1,13.37a1.55,1.55,0,0,0,.13.63,1.63,1.63,0,0,0,.86.86,1.55,1.55,0,0,0,.63.13,1.62,1.62,0,0,0,1.15-.48l6.69-6.68.52.11A3,3,0,0,0,11.5,8Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
4
extensions/notebook/resources/jupyter_config/custom.js
Normal file
4
extensions/notebook/resources/jupyter_config/custom.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// Make sure that all links load in the same tab
|
||||
define(['base/js/namespace'], function (Jupyter) {
|
||||
Jupyter._target = '_self';
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
# Disable CSP in order to load Jupyter inside Azure Data Studio
|
||||
c.NotebookApp.tornado_settings = {
|
||||
'headers': {'Content-Security-Policy': ''}
|
||||
}
|
||||
|
||||
c.NotebookApp.open_browser = False
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"kernel_python_credentials": {
|
||||
"url": "",
|
||||
"auth": "None"
|
||||
},
|
||||
"kernel_scala_credentials": {
|
||||
"url": "",
|
||||
"auth": "None"
|
||||
},
|
||||
"kernel_r_credentials": {
|
||||
"url": "",
|
||||
"auth": "None"
|
||||
},
|
||||
"ignore_ssl_errors": true,
|
||||
"logging_config": {
|
||||
"version": 1,
|
||||
"formatters": {
|
||||
"magicsFormatter": {
|
||||
"format": "%(asctime)s\t%(levelname)s\t%(message)s",
|
||||
"datefmt": ""
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"magicsHandler": {
|
||||
"class": "hdijupyterutils.filehandler.MagicsFileHandler",
|
||||
"formatter": "magicsFormatter",
|
||||
"home_path": ""
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"magicsLogger": {
|
||||
"handlers": [
|
||||
"magicsHandler"
|
||||
],
|
||||
"level": "DEBUG",
|
||||
"propagate": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
extensions/notebook/resources/light/manage.svg
Normal file
1
extensions/notebook/resources/light/manage.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>configure</title><path d="M15.08,1.71c.14.23.26.46.37.67a5.88,5.88,0,0,1,.29.65,4,4,0,0,1,.19.68,4.27,4.27,0,0,1,.07.78,4.43,4.43,0,0,1-.16,1.19,4.51,4.51,0,0,1-3.15,3.15A4.43,4.43,0,0,1,11.5,9l-.36,0-.36,0-6.3,6.3a2.56,2.56,0,0,1-.86.57,2.65,2.65,0,0,1-1,.2,2.53,2.53,0,0,1-1-.21A2.65,2.65,0,0,1,.21,14.39a2.53,2.53,0,0,1-.21-1,2.65,2.65,0,0,1,.2-1,2.56,2.56,0,0,1,.57-.86l6.3-6.3Q7,5,7,4.86c0-.12,0-.24,0-.36A4.43,4.43,0,0,1,7.16,3.3,4.51,4.51,0,0,1,10.31.15,4.43,4.43,0,0,1,11.5,0a4.27,4.27,0,0,1,.78.07A4,4,0,0,1,13,.25a5.88,5.88,0,0,1,.65.29l.67.37L11.2,4l.8.8ZM11.5,8a3.38,3.38,0,0,0,1.36-.28,3.53,3.53,0,0,0,1.86-1.86A3.38,3.38,0,0,0,15,4.49a3.29,3.29,0,0,0-.19-1.1L12,6.19,9.8,4l2.8-2.81A3.29,3.29,0,0,0,11.5,1a3.38,3.38,0,0,0-1.36.28A3.53,3.53,0,0,0,8.28,3.13,3.38,3.38,0,0,0,8,4.49,3,3,0,0,0,8,5q0,.26.11.52L1.48,12.23A1.62,1.62,0,0,0,1,13.37a1.55,1.55,0,0,0,.13.63,1.63,1.63,0,0,0,.86.86,1.55,1.55,0,0,0,.63.13,1.62,1.62,0,0,0,1.15-.48l6.69-6.68.52.11A3,3,0,0,0,11.5,8Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
82
extensions/notebook/src/common/apiWrapper.ts
Normal file
82
extensions/notebook/src/common/apiWrapper.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as sqlops from 'sqlops';
|
||||
|
||||
/**
|
||||
* Wrapper class to act as a facade over VSCode and Data APIs and allow us to test / mock callbacks into
|
||||
* this API from our code
|
||||
*
|
||||
* @export
|
||||
* @class ApiWrapper
|
||||
*/
|
||||
export class ApiWrapper {
|
||||
public createOutputChannel(name: string): vscode.OutputChannel {
|
||||
return vscode.window.createOutputChannel(name);
|
||||
}
|
||||
|
||||
public createTerminalWithOptions(options: vscode.TerminalOptions): vscode.Terminal {
|
||||
return vscode.window.createTerminal(options);
|
||||
}
|
||||
|
||||
public getCurrentConnection(): Thenable<sqlops.connection.Connection> {
|
||||
return sqlops.connection.getCurrentConnection();
|
||||
}
|
||||
|
||||
public getWorkspacePathFromUri(uri: vscode.Uri): string | undefined {
|
||||
let workspaceFolder = vscode.workspace.getWorkspaceFolder(uri);
|
||||
return workspaceFolder ? workspaceFolder.uri.fsPath : undefined;
|
||||
}
|
||||
|
||||
public registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): vscode.Disposable {
|
||||
return vscode.commands.registerCommand(command, callback, thisArg);
|
||||
}
|
||||
|
||||
public registerCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, ...triggerCharacters: string[]): vscode.Disposable {
|
||||
return vscode.languages.registerCompletionItemProvider(selector, provider, ...triggerCharacters);
|
||||
}
|
||||
|
||||
public registerTaskHandler(taskId: string, handler: (profile: sqlops.IConnectionProfile) => void): void {
|
||||
sqlops.tasks.registerTask(taskId, handler);
|
||||
}
|
||||
|
||||
public showErrorMessage(message: string, ...items: string[]): Thenable<string | undefined> {
|
||||
return vscode.window.showErrorMessage(message, ...items);
|
||||
}
|
||||
|
||||
public showOpenDialog(options: vscode.OpenDialogOptions): Thenable<vscode.Uri[] | undefined> {
|
||||
return vscode.window.showOpenDialog(options);
|
||||
}
|
||||
|
||||
public startBackgroundOperation(operationInfo: sqlops.BackgroundOperationInfo): void {
|
||||
sqlops.tasks.startBackgroundOperation(operationInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configuration for a extensionName
|
||||
* @param extensionName The string name of the extension to get the configuration for
|
||||
* @param resource The optional URI, as a URI object or a string, to use to get resource-scoped configurations
|
||||
*/
|
||||
public getConfiguration(extensionName?: string, resource?: vscode.Uri | string): vscode.WorkspaceConfiguration {
|
||||
if (typeof resource === 'string') {
|
||||
try {
|
||||
resource = this.parseUri(resource);
|
||||
} catch (e) {
|
||||
resource = undefined;
|
||||
}
|
||||
} else if (!resource) {
|
||||
// Fix to avoid adding lots of errors to debug console. Expects a valid resource or null, not undefined
|
||||
resource = null;
|
||||
}
|
||||
return vscode.workspace.getConfiguration(extensionName, resource as vscode.Uri);
|
||||
}
|
||||
|
||||
public parseUri(uri: string): vscode.Uri {
|
||||
return vscode.Uri.parse(uri);
|
||||
}
|
||||
}
|
||||
28
extensions/notebook/src/common/appContext.ts
Normal file
28
extensions/notebook/src/common/appContext.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { ApiWrapper } from './apiWrapper';
|
||||
|
||||
/**
|
||||
* Global context for the application
|
||||
*/
|
||||
export class AppContext {
|
||||
|
||||
private serviceMap: Map<string, any> = new Map();
|
||||
constructor(public readonly extensionContext: vscode.ExtensionContext, public readonly apiWrapper: ApiWrapper) {
|
||||
this.apiWrapper = apiWrapper || new ApiWrapper();
|
||||
}
|
||||
|
||||
public getService<T>(serviceName: string): T {
|
||||
return this.serviceMap.get(serviceName) as T;
|
||||
}
|
||||
|
||||
public registerService<T>(serviceName: string, service: T): void {
|
||||
this.serviceMap.set(serviceName, service);
|
||||
}
|
||||
}
|
||||
48
extensions/notebook/src/common/constants.ts
Normal file
48
extensions/notebook/src/common/constants.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
'use strict';
|
||||
|
||||
// CONFIG VALUES ///////////////////////////////////////////////////////////
|
||||
export const extensionConfigSectionName = 'dataManagement';
|
||||
export const extensionOutputChannel = 'SQL Server 2019 Preview';
|
||||
export const configLogDebugInfo = 'logDebugInfo';
|
||||
|
||||
// JUPYTER CONFIG //////////////////////////////////////////////////////////
|
||||
export const pythonBundleVersion = '0.0.1';
|
||||
export const pythonVersion = '3.6.6';
|
||||
export const sparkMagicVersion = '0.12.6.1';
|
||||
export const python3 = 'python3';
|
||||
export const pysparkkernel = 'pysparkkernel';
|
||||
export const sparkkernel = 'sparkkernel';
|
||||
export const pyspark3kernel = 'pyspark3kernel';
|
||||
export const python3DisplayName = 'Python 3';
|
||||
export const defaultSparkKernel = 'pyspark3kernel';
|
||||
export const pythonPathConfigKey = 'pythonPath';
|
||||
export const notebookConfigKey = 'notebook';
|
||||
|
||||
export const outputChannelName = 'dataManagement';
|
||||
export const hdfsHost = 'host';
|
||||
export const hdfsUser = 'user';
|
||||
|
||||
export const winPlatform = 'win32';
|
||||
|
||||
export const jupyterNotebookProviderId = 'jupyter';
|
||||
export const jupyterConfigRootFolder = 'jupyter_config';
|
||||
export const jupyterKernelsMasterFolder = 'kernels_master';
|
||||
export const jupyterNotebookLanguageId = 'jupyter-notebook';
|
||||
export const jupyterNotebookViewType = 'jupyter-notebook';
|
||||
export const jupyterNewNotebookTask = 'jupyter.task.newNotebook';
|
||||
export const jupyterOpenNotebookTask = 'jupyter.task.openNotebook';
|
||||
export const jupyterNewNotebookCommand = 'jupyter.cmd.newNotebook';
|
||||
export const jupyterCommandSetContext = 'jupyter.setContext';
|
||||
export const jupyterCommandSetKernel = 'jupyter.setKernel';
|
||||
export const jupyterReinstallDependenciesCommand = 'jupyter.reinstallDependencies';
|
||||
export const jupyterAnalyzeCommand = 'jupyter.cmd.analyzeNotebook';
|
||||
export const jupyterInstallPackages = 'jupyter.cmd.installPackages';
|
||||
export const jupyterConfigurePython = 'jupyter.cmd.configurePython';
|
||||
|
||||
export enum BuiltInCommands {
|
||||
SetContext = 'setContext'
|
||||
}
|
||||
|
||||
export enum CommandContext {
|
||||
WizardServiceEnabled = 'wizardservice:enabled'
|
||||
}
|
||||
19
extensions/notebook/src/common/localizedConstants.ts
Normal file
19
extensions/notebook/src/common/localizedConstants.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
// General Constants ///////////////////////////////////////////////////////
|
||||
export const msgYes = localize('msgYes', 'Yes');
|
||||
export const msgNo = localize('msgNo', 'No');
|
||||
|
||||
// Jupyter Constants ///////////////////////////////////////////////////////
|
||||
export const msgManagePackagesPowershell = localize('msgManagePackagesPowershell', '<#\n--------------------------------------------------------------------------------\n\tThis is the sandboxed instance of python used by Jupyter server.\n\tTo install packages used by the python kernel use \'.\\python.exe -m pip install\'\n--------------------------------------------------------------------------------\n#>');
|
||||
export const msgManagePackagesBash = localize('msgJupyterManagePackagesBash', ': \'\n--------------------------------------------------------------------------------\n\tThis is the sandboxed instance of python used by Jupyter server.\n\tTo install packages used by the python kernel use \'./python3.6 -m pip install\'\n--------------------------------------------------------------------------------\n\'');
|
||||
export const msgManagePackagesCmd = localize('msgJupyterManagePackagesCmd', 'REM This is the sandboxed instance of python used by Jupyter server. To install packages used by the python kernel use \'.\\python.exe -m pip install\'');
|
||||
export const msgSampleCodeDataFrame = localize('msgSampleCodeDataFrame', 'This sample code loads the file into a data frame and shows the first 10 results.');
|
||||
24
extensions/notebook/src/common/notebookUtils.ts
Normal file
24
extensions/notebook/src/common/notebookUtils.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
/**
|
||||
* Creates a random token per https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback.
|
||||
* Defaults to 24 bytes, which creates a 48-char hex string
|
||||
*/
|
||||
export function getRandomToken(size: number = 24): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
crypto.randomBytes(size, (err, buffer) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
let token = buffer.toString('hex');
|
||||
resolve(token);
|
||||
});
|
||||
});
|
||||
}
|
||||
115
extensions/notebook/src/common/ports.ts
Normal file
115
extensions/notebook/src/common/ports.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// This code is originally from https://github.com/Microsoft/vscode/blob/master/src/vs/base/node/ports.ts
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as net from 'net';
|
||||
|
||||
export class StrictPortFindOptions {
|
||||
constructor(public startPort: number, public minPort: number, public maxport: number) {
|
||||
}
|
||||
public maxRetriesPerStartPort: number = 5;
|
||||
public totalRetryLoops: number = 10;
|
||||
public timeout: number = 5000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for a free port with additional retries and a function to search in a much larger range if initial
|
||||
* attempt to find a port fails. By skipping to a random port after the first time failing, this should help
|
||||
* reduce the likelihood that no free port can be found.
|
||||
*/
|
||||
export async function strictFindFreePort(options: StrictPortFindOptions): Promise<number> {
|
||||
let totalRetries = options.totalRetryLoops;
|
||||
let startPort = options.startPort;
|
||||
let port = await findFreePort(startPort, options.maxRetriesPerStartPort, options.timeout);
|
||||
while (port === 0 && totalRetries > 0) {
|
||||
startPort = getRandomInt(options.minPort, options.maxport);
|
||||
port = await findFreePort(startPort, options.maxRetriesPerStartPort, options.timeout);
|
||||
totalRetries--;
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a random integer between `min` and `max`.
|
||||
*
|
||||
* @param {number} min - min number
|
||||
* @param {number} max - max number
|
||||
* @return {number} a random integer
|
||||
*/
|
||||
function getRandomInt(min, max): number {
|
||||
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a start point and a max number of retries, will find a port that
|
||||
* is openable. Will return 0 in case no free port can be found.
|
||||
*/
|
||||
export function findFreePort(startPort: number, giveUpAfter: number, timeout: number): Thenable<number> {
|
||||
let done = false;
|
||||
|
||||
return new Promise(resolve => {
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
return resolve(0);
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
doFindFreePort(startPort, giveUpAfter, (port) => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
clearTimeout(timeoutHandle);
|
||||
return resolve(port);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function doFindFreePort(startPort: number, giveUpAfter: number, clb: (port: number) => void): void {
|
||||
if (giveUpAfter === 0) {
|
||||
return clb(0);
|
||||
}
|
||||
|
||||
const client = new net.Socket();
|
||||
|
||||
// If we can connect to the port it means the port is already taken so we continue searching
|
||||
client.once('connect', () => {
|
||||
dispose(client);
|
||||
|
||||
return doFindFreePort(startPort + 1, giveUpAfter - 1, clb);
|
||||
});
|
||||
|
||||
client.once('data', () => {
|
||||
// this listener is required since node.js 8.x
|
||||
});
|
||||
|
||||
client.once('error', (err: Error & { code?: string }) => {
|
||||
dispose(client);
|
||||
|
||||
// If we receive any non ECONNREFUSED error, it means the port is used but we cannot connect
|
||||
if (err.code !== 'ECONNREFUSED') {
|
||||
return doFindFreePort(startPort + 1, giveUpAfter - 1, clb);
|
||||
}
|
||||
|
||||
// Otherwise it means the port is free to use!
|
||||
return clb(startPort);
|
||||
});
|
||||
|
||||
client.connect(startPort, '127.0.0.1');
|
||||
}
|
||||
|
||||
function dispose(socket: net.Socket): void {
|
||||
try {
|
||||
socket.removeAllListeners('connect');
|
||||
socket.removeAllListeners('error');
|
||||
socket.end();
|
||||
socket.destroy();
|
||||
socket.unref();
|
||||
} catch (error) {
|
||||
console.error(error); // otherwise this error would get lost in the callback chain
|
||||
}
|
||||
}
|
||||
26
extensions/notebook/src/common/promise.ts
Normal file
26
extensions/notebook/src/common/promise.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Deferred promise
|
||||
*/
|
||||
export class Deferred<T> {
|
||||
promise: Promise<T>;
|
||||
resolve: (value?: T | PromiseLike<T>) => void;
|
||||
reject: (reason?: any) => void;
|
||||
constructor() {
|
||||
this.promise = new Promise<T>((resolve, reject) => {
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
});
|
||||
}
|
||||
|
||||
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult>;
|
||||
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => void): Thenable<TResult>;
|
||||
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult> {
|
||||
return this.promise.then(onfulfilled, onrejected);
|
||||
}
|
||||
}
|
||||
126
extensions/notebook/src/common/utils.ts
Normal file
126
extensions/notebook/src/common/utils.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
'use strict';
|
||||
|
||||
import * as childProcess from 'child_process';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export function getKnoxUrl(host: string, port: string): string {
|
||||
return `https://${host}:${port}/gateway`;
|
||||
}
|
||||
|
||||
export function getLivyUrl(serverName: string, port: string): string {
|
||||
return this.getKnoxUrl(serverName, port) + '/default/livy/v1/';
|
||||
}
|
||||
|
||||
export async function mkDir(dirPath: string, outputChannel?: vscode.OutputChannel): Promise<void> {
|
||||
if (!await fs.exists(dirPath)) {
|
||||
if (outputChannel) {
|
||||
outputChannel.appendLine(localize('mkdirOutputMsg', '... Creating {0}', dirPath));
|
||||
}
|
||||
await fs.ensureDir(dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
export function getErrorMessage(error: Error | string): string {
|
||||
return (error instanceof Error) ? error.message : error;
|
||||
}
|
||||
|
||||
// COMMAND EXECUTION HELPERS ///////////////////////////////////////////////
|
||||
export function executeBufferedCommand(cmd: string, options: childProcess.ExecOptions, outputChannel?: vscode.OutputChannel): Thenable<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
if (outputChannel) {
|
||||
outputChannel.appendLine(` > ${cmd}`);
|
||||
}
|
||||
|
||||
let child = childProcess.exec(cmd, options, (err, stdout) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
|
||||
// Add listeners to print stdout and stderr if an output channel was provided
|
||||
if (outputChannel) {
|
||||
child.stdout.on('data', data => { outputDataChunk(data, outputChannel, ' stdout: '); });
|
||||
child.stderr.on('data', data => { outputDataChunk(data, outputChannel, ' stderr: '); });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function executeStreamedCommand(cmd: string, outputChannel?: vscode.OutputChannel): Thenable<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
// Start the command
|
||||
if (outputChannel) {
|
||||
outputChannel.appendLine(` > ${cmd}`);
|
||||
}
|
||||
let child = childProcess.spawn(cmd, [], { shell: true, detached: false });
|
||||
|
||||
// Add listeners to resolve/reject the promise on exit
|
||||
child.on('error', reject);
|
||||
child.on('exit', (code: number) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(localize('executeCommandProcessExited', 'Process exited with code {0}', code));
|
||||
}
|
||||
});
|
||||
|
||||
// Add listeners to print stdout and stderr if an output channel was provided
|
||||
if (outputChannel) {
|
||||
child.stdout.on('data', data => { outputDataChunk(data, outputChannel, ' stdout: '); });
|
||||
child.stderr.on('data', data => { outputDataChunk(data, outputChannel, ' stderr: '); });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getUserHome(): string {
|
||||
return process.env.HOME || process.env.USERPROFILE;
|
||||
}
|
||||
|
||||
export enum Platform {
|
||||
Mac,
|
||||
Linux,
|
||||
Windows,
|
||||
Others
|
||||
}
|
||||
|
||||
export function getOSPlatform(): Platform {
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
return Platform.Windows;
|
||||
case 'darwin':
|
||||
return Platform.Mac;
|
||||
case 'linux':
|
||||
return Platform.Linux;
|
||||
default:
|
||||
return Platform.Others;
|
||||
}
|
||||
}
|
||||
|
||||
export function getOSPlatformId(): string {
|
||||
var platformId = undefined;
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
platformId = 'win-x64';
|
||||
break;
|
||||
case 'darwin':
|
||||
platformId = 'osx';
|
||||
break;
|
||||
default:
|
||||
platformId = 'linux-x64';
|
||||
break;
|
||||
}
|
||||
return platformId;
|
||||
}
|
||||
|
||||
// PRIVATE HELPERS /////////////////////////////////////////////////////////
|
||||
function outputDataChunk(data: string | Buffer, outputChannel: vscode.OutputChannel, header: string): void {
|
||||
data.toString().split(/\r?\n/)
|
||||
.forEach(line => {
|
||||
outputChannel.appendLine(header + line);
|
||||
});
|
||||
}
|
||||
66
extensions/notebook/src/contracts/content.ts
Normal file
66
extensions/notebook/src/contracts/content.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
export interface INotebook {
|
||||
|
||||
readonly cells: ICell[];
|
||||
readonly metadata: INotebookMetadata;
|
||||
readonly nbformat: number;
|
||||
readonly nbformat_minor: number;
|
||||
}
|
||||
|
||||
export interface INotebookMetadata {
|
||||
kernelspec: IKernelInfo;
|
||||
language_info?: ILanguageInfo;
|
||||
}
|
||||
|
||||
export interface IKernelInfo {
|
||||
name: string;
|
||||
language?: string;
|
||||
display_name?: string;
|
||||
}
|
||||
|
||||
export interface ILanguageInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
mimetype?: string;
|
||||
codemirror_mode?: string | ICodeMirrorMode;
|
||||
}
|
||||
|
||||
export interface ICodeMirrorMode {
|
||||
name: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface ICell {
|
||||
cell_type: CellType;
|
||||
source: string | string[];
|
||||
metadata: {
|
||||
language?: string;
|
||||
};
|
||||
execution_count: number;
|
||||
outputs?: ICellOutput[];
|
||||
}
|
||||
|
||||
export type CellType = 'code' | 'markdown' | 'raw';
|
||||
|
||||
export class CellTypes {
|
||||
public static readonly Code = 'code';
|
||||
public static readonly Markdown = 'markdown';
|
||||
public static readonly Raw = 'raw';
|
||||
}
|
||||
|
||||
export interface ICellOutput {
|
||||
output_type: OutputType;
|
||||
}
|
||||
|
||||
export type OutputType =
|
||||
| 'execute_result'
|
||||
| 'display_data'
|
||||
| 'stream'
|
||||
| 'error'
|
||||
| 'update_display_data';
|
||||
171
extensions/notebook/src/dialog/configurePythonDialog.ts
Normal file
171
extensions/notebook/src/dialog/configurePythonDialog.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as fs from 'fs';
|
||||
import * as utils from '../common/utils';
|
||||
|
||||
import { AppContext } from '../common/appContext';
|
||||
import JupyterServerInstallation from '../jupyter/jupyterServerInstallation';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export class ConfigurePythonDialog {
|
||||
private dialog: sqlops.window.modelviewdialog.Dialog;
|
||||
|
||||
private readonly DialogTitle = localize('configurePython.dialogName', 'Configure Python for Notebooks');
|
||||
private readonly OkButtonText = localize('configurePython.okButtonText', 'Install');
|
||||
private readonly CancelButtonText = localize('configurePython.cancelButtonText', 'Cancel');
|
||||
private readonly BrowseButtonText = localize('configurePython.browseButtonText', 'Change location');
|
||||
private readonly LocationTextBoxTitle = localize('configurePython.locationTextBoxText', 'Notebook dependencies will be installed in this location');
|
||||
private readonly SelectFileLabel = localize('configurePython.selectFileLabel', 'Select');
|
||||
private readonly InstallationNote = localize('configurePython.installNote', 'This installation will take some time. It is recommended to not close the application until the installation is complete.');
|
||||
private readonly InvalidLocationMsg = localize('configurePython.invalidLocationMsg', 'The specified install location is invalid.');
|
||||
|
||||
private pythonLocationTextBox: sqlops.InputBoxComponent;
|
||||
private browseButton: sqlops.ButtonComponent;
|
||||
|
||||
constructor(private appContext: AppContext, private outputChannel: vscode.OutputChannel, private jupyterInstallation: JupyterServerInstallation) {
|
||||
}
|
||||
|
||||
public async showDialog() {
|
||||
this.dialog = sqlops.window.modelviewdialog.createDialog(this.DialogTitle);
|
||||
|
||||
this.initializeContent();
|
||||
|
||||
this.dialog.okButton.label = this.OkButtonText;
|
||||
this.dialog.cancelButton.label = this.CancelButtonText;
|
||||
|
||||
this.dialog.registerCloseValidator(() => this.handleInstall());
|
||||
|
||||
sqlops.window.modelviewdialog.openDialog(this.dialog);
|
||||
}
|
||||
|
||||
private initializeContent() {
|
||||
this.dialog.registerContent(async view => {
|
||||
this.pythonLocationTextBox = view.modelBuilder.inputBox()
|
||||
.withProperties<sqlops.InputBoxProperties>({
|
||||
value: JupyterServerInstallation.getPythonInstallPath(this.appContext.apiWrapper),
|
||||
width: '100%'
|
||||
}).component();
|
||||
|
||||
this.browseButton = view.modelBuilder.button()
|
||||
.withProperties<sqlops.ButtonProperties>({
|
||||
label: this.BrowseButtonText,
|
||||
width: '100px'
|
||||
}).component();
|
||||
this.browseButton.onDidClick(() => this.handleBrowse());
|
||||
|
||||
let installationNoteText = view.modelBuilder.text().withProperties({
|
||||
value: this.InstallationNote
|
||||
}).component();
|
||||
let noteWrapper = view.modelBuilder.flexContainer().component();
|
||||
noteWrapper.addItem(installationNoteText, {
|
||||
flex: '1 1 auto',
|
||||
CSSStyles: {
|
||||
'margin-top': '60px',
|
||||
'padding-left': '15px',
|
||||
'padding-right': '15px',
|
||||
'border': '1px solid'
|
||||
}
|
||||
});
|
||||
|
||||
let formModel = view.modelBuilder.formContainer()
|
||||
.withFormItems([{
|
||||
component: this.pythonLocationTextBox,
|
||||
title: this.LocationTextBoxTitle
|
||||
}, {
|
||||
component: this.browseButton,
|
||||
title: undefined
|
||||
}, {
|
||||
component: noteWrapper,
|
||||
title: undefined
|
||||
}]).component();
|
||||
|
||||
|
||||
await view.initializeModel(formModel);
|
||||
});
|
||||
}
|
||||
|
||||
private async handleInstall(): Promise<boolean> {
|
||||
let pythonLocation = this.pythonLocationTextBox.value;
|
||||
if (!pythonLocation || pythonLocation.length === 0) {
|
||||
this.showErrorMessage(this.InvalidLocationMsg);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
let isValid = await this.isFileValid(pythonLocation);
|
||||
if (!isValid) {
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
this.appContext.apiWrapper.showErrorMessage(utils.getErrorMessage(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't wait on installation, since there's currently no Cancel functionality
|
||||
this.jupyterInstallation.startInstallProcess(pythonLocation).catch(err => {
|
||||
this.appContext.apiWrapper.showErrorMessage(utils.getErrorMessage(err));
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private isFileValid(pythonLocation: string): Promise<boolean> {
|
||||
let self = this;
|
||||
return new Promise<boolean>(function (resolve) {
|
||||
fs.stat(pythonLocation, function (err, stats) {
|
||||
if (err) {
|
||||
// Ignore error if folder doesn't exist, since it will be
|
||||
// created during installation
|
||||
if (err.code !== 'ENOENT') {
|
||||
self.showErrorMessage(err.message);
|
||||
resolve(false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (stats.isFile()) {
|
||||
self.showErrorMessage(self.InvalidLocationMsg);
|
||||
resolve(false);
|
||||
}
|
||||
}
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async handleBrowse(): Promise<void> {
|
||||
let options: vscode.OpenDialogOptions = {
|
||||
defaultUri: vscode.Uri.file(utils.getUserHome()),
|
||||
canSelectFiles: false,
|
||||
canSelectFolders: true,
|
||||
canSelectMany: false,
|
||||
openLabel: this.SelectFileLabel
|
||||
};
|
||||
|
||||
let fileUris: vscode.Uri[] = await this.appContext.apiWrapper.showOpenDialog(options);
|
||||
if (fileUris && fileUris[0]) {
|
||||
this.pythonLocationTextBox.value = fileUris[0].fsPath;
|
||||
}
|
||||
}
|
||||
|
||||
private showInfoMessage(message: string) {
|
||||
this.dialog.message = {
|
||||
text: message,
|
||||
level: sqlops.window.modelviewdialog.MessageLevel.Information
|
||||
};
|
||||
}
|
||||
|
||||
private showErrorMessage(message: string) {
|
||||
this.dialog.message = {
|
||||
text: message,
|
||||
level: sqlops.window.modelviewdialog.MessageLevel.Error
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,11 @@ import * as vscode from 'vscode';
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as os from 'os';
|
||||
import * as nls from 'vscode-nls';
|
||||
|
||||
import { JupyterController } from './jupyter/jupyterController';
|
||||
import { AppContext } from './common/appContext';
|
||||
import { ApiWrapper } from './common/apiWrapper';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
const JUPYTER_NOTEBOOK_PROVIDER = 'jupyter';
|
||||
@@ -17,6 +22,8 @@ const noNotebookVisible = localize('noNotebookVisible', 'No notebook editor is a
|
||||
|
||||
let counter = 0;
|
||||
|
||||
export let controller: JupyterController;
|
||||
|
||||
export function activate(extensionContext: vscode.ExtensionContext) {
|
||||
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.new', (connectionId?: string) => {
|
||||
newNotebook(connectionId);
|
||||
@@ -37,6 +44,9 @@ export function activate(extensionContext: vscode.ExtensionContext) {
|
||||
analyzeNotebook(explorerContext);
|
||||
}));
|
||||
|
||||
let appContext = new AppContext(extensionContext, new ApiWrapper());
|
||||
controller = new JupyterController(appContext);
|
||||
controller.activate();
|
||||
}
|
||||
|
||||
function newNotebook(connectionId: string) {
|
||||
@@ -141,4 +151,7 @@ async function analyzeNotebook(oeContext?: sqlops.ObjectExplorerContext): Promis
|
||||
|
||||
// this method is called when your extension is deactivated
|
||||
export function deactivate() {
|
||||
if (controller) {
|
||||
controller.deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as should from 'should';
|
||||
import * as assert from 'assert';
|
||||
import * as vscode from 'vscode';
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as tempWrite from 'temp-write';
|
||||
import 'mocha';
|
||||
|
||||
import { JupyterController } from '../jupyter/jupyterController';
|
||||
import { INotebook, CellTypes } from '../contracts/content';
|
||||
|
||||
describe('Notebook Integration Test', function (): void {
|
||||
this.timeout(600000);
|
||||
|
||||
let expectedNotebookContent: INotebook = {
|
||||
cells: [{
|
||||
cell_type: CellTypes.Code,
|
||||
source: '1+1',
|
||||
metadata: { language: 'python' },
|
||||
execution_count: 1
|
||||
}],
|
||||
metadata: {
|
||||
'kernelspec': {
|
||||
'name': 'pyspark3kernel',
|
||||
'display_name': 'PySpark3'
|
||||
}
|
||||
},
|
||||
nbformat: 4,
|
||||
nbformat_minor: 2
|
||||
};
|
||||
|
||||
|
||||
it('Should connect to local notebook server with result 2', async function () {
|
||||
this.timeout(60000);
|
||||
let pythonNotebook = Object.assign({}, expectedNotebookContent, { metadata: { kernelspec: { name: 'python3', display_name: 'Python 3' } } });
|
||||
let uri = writeNotebookToFile(pythonNotebook);
|
||||
await ensureJupyterInstalled();
|
||||
|
||||
let notebook = await sqlops.nb.showNotebookDocument(uri);
|
||||
should(notebook.document.cells).have.length(1);
|
||||
let ran = await notebook.runCell(notebook.document.cells[0]);
|
||||
should(ran).be.true('Notebook runCell failed');
|
||||
let cellOutputs = notebook.document.cells[0].contents.outputs;
|
||||
should(cellOutputs).have.length(1);
|
||||
let result = (<sqlops.nb.IExecuteResult>cellOutputs[0]).data['text/plain'];
|
||||
should(result).equal('2');
|
||||
|
||||
try {
|
||||
// TODO support closing the editor. Right now this prompts and there's no override for this. Need to fix in core
|
||||
// Close the editor using the recommended vscode API
|
||||
//await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
|
||||
}
|
||||
catch (e) { }
|
||||
});
|
||||
|
||||
it('Should connect to remote spark server with result 2', async function () {
|
||||
this.timeout(240000);
|
||||
let uri = writeNotebookToFile(expectedNotebookContent);
|
||||
await ensureJupyterInstalled();
|
||||
|
||||
// Given a connection to a server exists
|
||||
let connectionId = await connectToSparkIntegrationServer();
|
||||
|
||||
// When I open a Spark notebook and run the cell
|
||||
let notebook = await sqlops.nb.showNotebookDocument(uri, {
|
||||
connectionId: connectionId
|
||||
});
|
||||
should(notebook.document.cells).have.length(1);
|
||||
let ran = await notebook.runCell(notebook.document.cells[0]);
|
||||
should(ran).be.true('Notebook runCell failed');
|
||||
|
||||
// Then I expect to get the output result of 1+1, executed remotely against the Spark endpoint
|
||||
let cellOutputs = notebook.document.cells[0].contents.outputs;
|
||||
should(cellOutputs).have.length(4);
|
||||
let sparkResult = (<sqlops.nb.IStreamResult>cellOutputs[3]).text;
|
||||
should(sparkResult).equal('2');
|
||||
|
||||
try {
|
||||
// TODO support closing the editor. Right now this prompts and there's no override for this. Need to fix in core
|
||||
// Close the editor using the recommended vscode API
|
||||
//await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
|
||||
}
|
||||
catch (e) { }
|
||||
});
|
||||
});
|
||||
|
||||
async function connectToSparkIntegrationServer(): Promise<string> {
|
||||
assert.ok(process.env.BACKEND_HOSTNAME, 'BACKEND_HOSTNAME, BACKEND_USERNAME, BACKEND_PWD must be set using ./tasks/setbackenvariables.sh or .\\tasks\\setbackendvaraibles.bat');
|
||||
let connInfo: sqlops.connection.Connection = {
|
||||
options: {
|
||||
'host': process.env.BACKEND_HOSTNAME,
|
||||
'groupId': 'C777F06B-202E-4480-B475-FA416154D458',
|
||||
'knoxport': '',
|
||||
'user': process.env.BACKEND_USERNAME,
|
||||
'password': process.env.BACKEND_PWD
|
||||
},
|
||||
providerName: 'HADOOP_KNOX',
|
||||
connectionId: 'abcd1234',
|
||||
};
|
||||
connInfo['savePassword'] = true;
|
||||
let result = await sqlops.connection.connect(<any>connInfo as sqlops.IConnectionProfile);
|
||||
|
||||
should(result.connected).be.true();
|
||||
should(result.connectionId).not.be.undefined();
|
||||
should(result.connectionId).not.be.empty();
|
||||
should(result.errorMessage).be.undefined();
|
||||
|
||||
let activeConnections = await sqlops.connection.getActiveConnections();
|
||||
should(activeConnections).have.length(1);
|
||||
|
||||
return result.connectionId;
|
||||
}
|
||||
|
||||
function writeNotebookToFile(pythonNotebook: INotebook): vscode.Uri {
|
||||
let notebookContentString = JSON.stringify(pythonNotebook);
|
||||
let localFile = tempWrite.sync(notebookContentString, 'notebook.ipynb');
|
||||
let uri = vscode.Uri.file(localFile);
|
||||
return uri;
|
||||
}
|
||||
|
||||
async function ensureJupyterInstalled(): Promise<void> {
|
||||
let jupterControllerExports = vscode.extensions.getExtension('Microsoft.sql-vnext').exports;
|
||||
let jupyterController = jupterControllerExports.getJupterController() as JupyterController;
|
||||
await jupyterController.jupyterInstallation;
|
||||
}
|
||||
|
||||
200
extensions/notebook/src/intellisense/completionItemProvider.ts
Normal file
200
extensions/notebook/src/intellisense/completionItemProvider.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { nb } from 'sqlops';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { charCountToJsCountDiff, jsIndexToCharIndex } from './text';
|
||||
import { JupyterNotebookProvider } from '../jupyter/jupyterNotebookProvider';
|
||||
import { JupyterSessionManager } from '../jupyter/jupyterSessionManager';
|
||||
import { Deferred } from '../common/promise';
|
||||
|
||||
const timeoutMilliseconds = 4000;
|
||||
|
||||
export class NotebookCompletionItemProvider implements vscode.CompletionItemProvider {
|
||||
private _allDocuments: nb.NotebookDocument[];
|
||||
private kernelDeferred = new Deferred<nb.IKernel>();
|
||||
|
||||
constructor(private _notebookProvider: JupyterNotebookProvider) {
|
||||
}
|
||||
|
||||
public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext)
|
||||
: vscode.ProviderResult<vscode.CompletionItem[] | vscode.CompletionList> {
|
||||
this._allDocuments = nb.notebookDocuments;
|
||||
let info = this.findMatchingCell(document);
|
||||
this.isNotConnected(document, info);
|
||||
// Get completions, with cancellation on timeout or if cancel is requested.
|
||||
// Note that it's important we always return some value, or intellisense will never complete
|
||||
let promises = [this.requestCompletions(info, position, document), this.onCanceled(token), this.onTimeout(timeoutMilliseconds)];
|
||||
return Promise.race(promises);
|
||||
}
|
||||
|
||||
public resolveCompletionItem(item: vscode.CompletionItem, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CompletionItem> {
|
||||
return item;
|
||||
}
|
||||
|
||||
private isNotConnected(document: vscode.TextDocument, info: INewIntellisenseInfo): void {
|
||||
if (!info || !this._notebookProvider) {
|
||||
return;
|
||||
}
|
||||
let notebookManager: nb.NotebookManager = undefined;
|
||||
|
||||
let kernel: nb.IKernel = undefined;
|
||||
try {
|
||||
this._notebookProvider.getNotebookManager(document.uri).then(manager => {
|
||||
notebookManager = manager;
|
||||
if (notebookManager) {
|
||||
let sessionManager: JupyterSessionManager = <JupyterSessionManager>(notebookManager.sessionManager);
|
||||
let sessions = sessionManager.listRunning();
|
||||
if (sessions && sessions.length > 0) {
|
||||
let session = sessions.find(session => session.path === info.notebook.uri.path);
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
kernel = session.kernel;
|
||||
}
|
||||
}
|
||||
this.kernelDeferred.resolve(kernel);
|
||||
});
|
||||
} catch {
|
||||
// If an exception occurs, swallow it currently
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private findMatchingCell(document: vscode.TextDocument): INewIntellisenseInfo {
|
||||
if (this._allDocuments && document) {
|
||||
for (let doc of this._allDocuments) {
|
||||
for (let cell of doc.cells) {
|
||||
if (cell && cell.uri && cell.uri.path === document.uri.path) {
|
||||
return {
|
||||
editorUri: cell.uri.path,
|
||||
cell: cell,
|
||||
notebook: doc
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async requestCompletions(info: INewIntellisenseInfo, position: vscode.Position, cellTextDocument: vscode.TextDocument): Promise<vscode.CompletionItem[]> {
|
||||
let kernel = await this.kernelDeferred.promise;
|
||||
this.kernelDeferred = new Deferred<nb.IKernel>();
|
||||
if (!info || kernel === undefined || !kernel.supportsIntellisense || !kernel.isReady) {
|
||||
return [];
|
||||
}
|
||||
let source = cellTextDocument.getText();
|
||||
if (!source || source.length === 0) {
|
||||
return [];
|
||||
}
|
||||
let cursorPosition = this.toCursorPosition(position, source);
|
||||
let result = await kernel.requestComplete({
|
||||
code: source,
|
||||
cursor_pos: cursorPosition.adjustedPosition
|
||||
});
|
||||
if (!result || !result.content || result.content.status === 'error') {
|
||||
return [];
|
||||
}
|
||||
let content = result.content;
|
||||
// Get position relative to the current cursor.
|
||||
let range = this.getEditRange(content, cursorPosition, position, source);
|
||||
let items: vscode.CompletionItem[] = content.matches.map(m => {
|
||||
let item: vscode.CompletionItem = {
|
||||
label: m,
|
||||
insertText: m,
|
||||
kind: vscode.CompletionItemKind.Text,
|
||||
textEdit: {
|
||||
range: range,
|
||||
newText: m,
|
||||
newEol: undefined
|
||||
}
|
||||
};
|
||||
return item;
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
private getEditRange(content: nb.ICompletionContent, cursorPosition: IRelativePosition, position: vscode.Position, source: string): vscode.Range {
|
||||
let relativeStart = this.getRelativeStart(content, cursorPosition, source);
|
||||
// For now we're not adjusting relativeEnd. This may be a subtle issue here: if this ever actually goes past the end character then we should probably
|
||||
// account for the difference on the right-hand-side of the original text
|
||||
let relativeEnd = content.cursor_end - cursorPosition.adjustedPosition;
|
||||
let range = new vscode.Range(
|
||||
new vscode.Position(position.line, Math.max(relativeStart + position.character, 0)),
|
||||
new vscode.Position(position.line, Math.max(relativeEnd + position.character, 0)));
|
||||
return range;
|
||||
}
|
||||
|
||||
private getRelativeStart(content: nb.ICompletionContent, cursorPosition: IRelativePosition, source: string): number {
|
||||
let relativeStart = 0;
|
||||
if (content.cursor_start !== cursorPosition.adjustedPosition) {
|
||||
// Account for possible surrogate characters inside the substring.
|
||||
// We need to examine the substring between (start, end) for surrogates and add 1 char for each of these.
|
||||
let diff = cursorPosition.adjustedPosition - content.cursor_start;
|
||||
let startIndex = cursorPosition.originalPosition - diff;
|
||||
let adjustedStart = content.cursor_start + charCountToJsCountDiff(source.slice(startIndex, cursorPosition.originalPosition));
|
||||
relativeStart = adjustedStart - cursorPosition.adjustedPosition;
|
||||
} else {
|
||||
// It didn't change so leave at 0
|
||||
relativeStart = 0;
|
||||
}
|
||||
return relativeStart;
|
||||
}
|
||||
|
||||
private onCanceled(token: vscode.CancellationToken): Promise<vscode.CompletionItem[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// On cancellation, quit
|
||||
token.onCancellationRequested(() => resolve([]));
|
||||
});
|
||||
}
|
||||
|
||||
private onTimeout(timeout: number): Promise<vscode.CompletionItem[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// After 4 seconds, quit
|
||||
setTimeout(() => resolve([]), timeout);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from a line+character position to a cursor position based on the whole string length
|
||||
* Note: this is somewhat inefficient especially for large arrays. However we've done
|
||||
* this for other intellisense libraries that are index based. The ideal would be to at
|
||||
* least do caching of the contents in an efficient lookup structure so we don't have to recalculate
|
||||
* and throw away each time.
|
||||
*/
|
||||
private toCursorPosition(position: vscode.Position, source: string): IRelativePosition {
|
||||
let lines = source.split('\n');
|
||||
let characterPosition = 0;
|
||||
let currentLine = 0;
|
||||
// Add up all lines up to the current one
|
||||
for (currentLine; currentLine < position.line; currentLine++) {
|
||||
// Add to the position, accounting for the \n at the end of the line
|
||||
characterPosition += lines[currentLine].length + 1;
|
||||
}
|
||||
// Then add up to the cursor position on that line
|
||||
characterPosition += position.character;
|
||||
// Return the sum
|
||||
return {
|
||||
originalPosition: characterPosition,
|
||||
adjustedPosition: jsIndexToCharIndex(characterPosition, source)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface IRelativePosition {
|
||||
originalPosition: number;
|
||||
adjustedPosition: number;
|
||||
}
|
||||
|
||||
|
||||
export interface INewIntellisenseInfo {
|
||||
editorUri: string;
|
||||
cell: nb.NotebookCell;
|
||||
notebook: nb.NotebookDocument;
|
||||
}
|
||||
72
extensions/notebook/src/intellisense/text.ts
Normal file
72
extensions/notebook/src/intellisense/text.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) Jupyter Development Team.
|
||||
// Distributed under the terms of the Modified BSD License.
|
||||
|
||||
// This code is originally from @jupyterlab/packages/coreutils/src/text.ts
|
||||
// Note: this code doesn't seem to do anything in the sqlops environment since the
|
||||
// surr
|
||||
|
||||
// javascript stores text as utf16 and string indices use "code units",
|
||||
// which stores high-codepoint characters as "surrogate pairs",
|
||||
// which occupy two indices in the javascript string.
|
||||
// We need to translate cursor_pos in the Jupyter protocol (in characters)
|
||||
// to js offset (with surrogate pairs taking two spots).
|
||||
|
||||
const HAS_SURROGATES: boolean = '𝐚'.length > 1;
|
||||
|
||||
/**
|
||||
* Convert a javascript string index into a unicode character offset
|
||||
*
|
||||
* @param jsIdx - The javascript string index (counting surrogate pairs)
|
||||
*
|
||||
* @param text - The text in which the offset is calculated
|
||||
*
|
||||
* @returns The unicode character offset
|
||||
*/
|
||||
export function jsIndexToCharIndex(jsIdx: number, text: string): number {
|
||||
if (!HAS_SURROGATES) {
|
||||
// not using surrogates, nothing to do
|
||||
return jsIdx;
|
||||
}
|
||||
let charIdx = jsIdx;
|
||||
for (let i = 0; i + 1 < text.length && i < jsIdx; i++) {
|
||||
let charCode = text.charCodeAt(i);
|
||||
// check for surrogate pair
|
||||
if (charCode >= 0xd800 && charCode <= 0xdbff) {
|
||||
let nextCharCode = text.charCodeAt(i + 1);
|
||||
if (nextCharCode >= 0xdc00 && nextCharCode <= 0xdfff) {
|
||||
charIdx--;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return charIdx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the diff between pure character count and JS-based count with 2 chars per surrogate pair.
|
||||
*
|
||||
* @param charIdx - The index in unicode characters
|
||||
*
|
||||
* @param text - The text in which the offset is calculated
|
||||
*
|
||||
* @returns The js-native index
|
||||
*/
|
||||
export function charCountToJsCountDiff(text: string): number {
|
||||
let diff = 0;
|
||||
if (!HAS_SURROGATES) {
|
||||
// not using surrogates, nothing to do
|
||||
return diff;
|
||||
}
|
||||
for (let i = 0; i + 1 < text.length; i++) {
|
||||
let charCode = text.charCodeAt(i);
|
||||
// check for surrogate pair
|
||||
if (charCode >= 0xd800 && charCode <= 0xdbff) {
|
||||
let nextCharCode = text.charCodeAt(i + 1);
|
||||
if (nextCharCode >= 0xdc00 && nextCharCode <= 0xdfff) {
|
||||
diff++;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return diff;
|
||||
}
|
||||
16
extensions/notebook/src/jupyter/common.ts
Normal file
16
extensions/notebook/src/jupyter/common.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export interface IServerInstance {
|
||||
readonly port: string;
|
||||
readonly uri: vscode.Uri;
|
||||
configure(): Promise<void>;
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
260
extensions/notebook/src/jupyter/jupyterController.ts
Normal file
260
extensions/notebook/src/jupyter/jupyterController.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as path from 'path';
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as vscode from 'vscode';
|
||||
import * as os from 'os';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import * as constants from '../common/constants';
|
||||
import * as localizedConstants from '../common/localizedConstants';
|
||||
import JupyterServerInstallation from './jupyterServerInstallation';
|
||||
import { IServerInstance } from './common';
|
||||
import * as utils from '../common/utils';
|
||||
import { IPrompter, QuestionTypes, IQuestion } from '../prompts/question';
|
||||
|
||||
import { AppContext } from '../common/appContext';
|
||||
import { ApiWrapper } from '../common/apiWrapper';
|
||||
import { LocalJupyterServerManager } from './jupyterServerManager';
|
||||
import { NotebookCompletionItemProvider } from '../intellisense/completionItemProvider';
|
||||
import { JupyterNotebookProvider } from './jupyterNotebookProvider';
|
||||
import { ConfigurePythonDialog } from '../dialog/configurePythonDialog';
|
||||
import CodeAdapter from '../prompts/adapter';
|
||||
|
||||
let untitledCounter = 0;
|
||||
|
||||
export class JupyterController implements vscode.Disposable {
|
||||
private _jupyterInstallation: Promise<JupyterServerInstallation>;
|
||||
private _notebookInstances: IServerInstance[] = [];
|
||||
|
||||
private outputChannel: vscode.OutputChannel;
|
||||
private prompter: IPrompter;
|
||||
|
||||
constructor(private appContext: AppContext) {
|
||||
this.prompter = new CodeAdapter();
|
||||
this.outputChannel = this.appContext.apiWrapper.createOutputChannel(constants.extensionOutputChannel);
|
||||
}
|
||||
|
||||
private get apiWrapper(): ApiWrapper {
|
||||
return this.appContext.apiWrapper;
|
||||
}
|
||||
|
||||
public get extensionContext(): vscode.ExtensionContext {
|
||||
return this.appContext && this.appContext.extensionContext;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.deactivate();
|
||||
}
|
||||
|
||||
// PUBLIC METHODS //////////////////////////////////////////////////////
|
||||
public async activate(): Promise<boolean> {
|
||||
// Prompt for install if the python installation path is not defined
|
||||
let jupyterInstaller = new JupyterServerInstallation(
|
||||
this.extensionContext.extensionPath,
|
||||
this.outputChannel,
|
||||
this.apiWrapper);
|
||||
if (JupyterServerInstallation.isPythonInstalled(this.apiWrapper)) {
|
||||
this._jupyterInstallation = Promise.resolve(jupyterInstaller);
|
||||
} else {
|
||||
this._jupyterInstallation = new Promise(resolve => {
|
||||
jupyterInstaller.onInstallComplete(err => {
|
||||
if (!err) {
|
||||
resolve(jupyterInstaller);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let notebookProvider = undefined;
|
||||
|
||||
notebookProvider = this.registerNotebookProvider();
|
||||
sqlops.nb.onDidOpenNotebookDocument(notebook => {
|
||||
if (!JupyterServerInstallation.isPythonInstalled(this.apiWrapper)) {
|
||||
this.doConfigurePython(jupyterInstaller);
|
||||
}
|
||||
});
|
||||
// Add command/task handlers
|
||||
this.apiWrapper.registerTaskHandler(constants.jupyterOpenNotebookTask, (profile: sqlops.IConnectionProfile) => {
|
||||
return this.handleOpenNotebookTask(profile);
|
||||
});
|
||||
this.apiWrapper.registerTaskHandler(constants.jupyterNewNotebookTask, (profile: sqlops.IConnectionProfile) => {
|
||||
return this.saveProfileAndCreateNotebook(profile);
|
||||
});
|
||||
this.apiWrapper.registerCommand(constants.jupyterNewNotebookCommand, (explorerContext: sqlops.ObjectExplorerContext) => {
|
||||
return this.saveProfileAndCreateNotebook(explorerContext ? explorerContext.connectionProfile : undefined);
|
||||
});
|
||||
this.apiWrapper.registerCommand(constants.jupyterAnalyzeCommand, (explorerContext: sqlops.ObjectExplorerContext) => {
|
||||
return this.saveProfileAndAnalyzeNotebook(explorerContext);
|
||||
});
|
||||
|
||||
this.apiWrapper.registerCommand(constants.jupyterReinstallDependenciesCommand, () => { return this.handleDependenciesReinstallation(); });
|
||||
this.apiWrapper.registerCommand(constants.jupyterInstallPackages, () => { return this.doManagePackages(); });
|
||||
this.apiWrapper.registerCommand(constants.jupyterConfigurePython, () => { return this.doConfigurePython(jupyterInstaller); });
|
||||
|
||||
let supportedFileFilter: vscode.DocumentFilter[] = [
|
||||
{ scheme: 'file', language: '*' },
|
||||
{ scheme: 'untitled', language: '*' }
|
||||
];
|
||||
this.extensionContext.subscriptions.push(this.apiWrapper.registerCompletionItemProvider(supportedFileFilter, new NotebookCompletionItemProvider(notebookProvider)));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private registerNotebookProvider(): JupyterNotebookProvider {
|
||||
let notebookProvider = new JupyterNotebookProvider((documentUri: vscode.Uri) => new LocalJupyterServerManager({
|
||||
documentPath: documentUri.fsPath,
|
||||
jupyterInstallation: this._jupyterInstallation,
|
||||
extensionContext: this.extensionContext,
|
||||
apiWrapper: this.apiWrapper
|
||||
}));
|
||||
sqlops.nb.registerNotebookProvider(notebookProvider);
|
||||
return notebookProvider;
|
||||
}
|
||||
|
||||
private saveProfileAndCreateNotebook(profile: sqlops.IConnectionProfile): Promise<void> {
|
||||
return this.handleNewNotebookTask(undefined, profile);
|
||||
}
|
||||
|
||||
private saveProfileAndAnalyzeNotebook(oeContext: sqlops.ObjectExplorerContext): Promise<void> {
|
||||
return this.handleNewNotebookTask(oeContext, oeContext.connectionProfile);
|
||||
}
|
||||
|
||||
public deactivate(): void {
|
||||
// Shutdown any open notebooks
|
||||
this._notebookInstances.forEach(instance => { instance.stop(); });
|
||||
}
|
||||
|
||||
// EVENT HANDLERS //////////////////////////////////////////////////////
|
||||
public async getDefaultConnection(): Promise<sqlops.ConnectionInfo> {
|
||||
return await this.apiWrapper.getCurrentConnection();
|
||||
}
|
||||
|
||||
private async handleOpenNotebookTask(profile: sqlops.IConnectionProfile): Promise<void> {
|
||||
let notebookFileTypeName = localize('notebookFileType', 'Notebooks');
|
||||
let filter = {};
|
||||
filter[notebookFileTypeName] = 'ipynb';
|
||||
let uris = await this.apiWrapper.showOpenDialog({
|
||||
filters: filter,
|
||||
canSelectFiles: true,
|
||||
canSelectMany: false
|
||||
});
|
||||
if (uris && uris.length > 0) {
|
||||
let fileUri = uris[0];
|
||||
// Verify this is a .ipynb file since this isn't actually filtered on Mac/Linux
|
||||
if (path.extname(fileUri.fsPath) !== '.ipynb') {
|
||||
// in the future might want additional supported types
|
||||
this.apiWrapper.showErrorMessage(localize('unsupportedFileType', 'Only .ipynb Notebooks are supported'));
|
||||
} else {
|
||||
await sqlops.nb.showNotebookDocument(fileUri, {
|
||||
connectionId: profile.id,
|
||||
providerId: constants.jupyterNotebookProviderId,
|
||||
preview: false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleNewNotebookTask(oeContext?: sqlops.ObjectExplorerContext, profile?: sqlops.IConnectionProfile): Promise<void> {
|
||||
// Ensure we get a unique ID for the notebook. For now we're using a different prefix to the built-in untitled files
|
||||
// to handle this. We should look into improving this in the future
|
||||
let untitledUri = vscode.Uri.parse(`untitled:Notebook-${untitledCounter++}`);
|
||||
let editor = await sqlops.nb.showNotebookDocument(untitledUri, {
|
||||
connectionId: profile.id,
|
||||
providerId: constants.jupyterNotebookProviderId,
|
||||
preview: false,
|
||||
defaultKernel: {
|
||||
name: 'pyspark3kernel',
|
||||
display_name: 'PySpark3',
|
||||
language: 'python'
|
||||
}
|
||||
});
|
||||
if (oeContext && oeContext.nodeInfo && oeContext.nodeInfo.nodePath) {
|
||||
// Get the file path after '/HDFS'
|
||||
let hdfsPath: string = oeContext.nodeInfo.nodePath.substring(oeContext.nodeInfo.nodePath.indexOf('/HDFS') + '/HDFS'.length);
|
||||
if (hdfsPath.length > 0) {
|
||||
let analyzeCommand = '#' + localizedConstants.msgSampleCodeDataFrame + os.EOL + 'df = (spark.read.option(\"inferSchema\", \"true\")'
|
||||
+ os.EOL + '.option(\"header\", \"true\")' + os.EOL + '.csv(\'{0}\'))' + os.EOL + 'df.show(10)';
|
||||
// TODO re-enable insert into document once APIs are finalized.
|
||||
// editor.document.cells[0].source = [analyzeCommand.replace('{0}', hdfsPath)];
|
||||
editor.edit(editBuilder => {
|
||||
editBuilder.replace(0, {
|
||||
cell_type: 'code',
|
||||
source: analyzeCommand.replace('{0}', hdfsPath)
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleDependenciesReinstallation(): Promise<void> {
|
||||
if (await this.confirmReinstall()) {
|
||||
this._jupyterInstallation = JupyterServerInstallation.getInstallation(
|
||||
this.extensionContext.extensionPath,
|
||||
this.outputChannel,
|
||||
this.apiWrapper,
|
||||
undefined,
|
||||
true);
|
||||
}
|
||||
}
|
||||
|
||||
//Confirmation message dialog
|
||||
private async confirmReinstall(): Promise<boolean> {
|
||||
return await this.prompter.promptSingle<boolean>(<IQuestion>{
|
||||
type: QuestionTypes.confirm,
|
||||
message: localize('confirmReinstall', 'Are you sure you want to reinstall?'),
|
||||
default: true
|
||||
});
|
||||
}
|
||||
|
||||
public doManagePackages(): void {
|
||||
try {
|
||||
let terminal = this.apiWrapper.createTerminalWithOptions({ cwd: this.getPythonBinDir() });
|
||||
terminal.show(true);
|
||||
let shellType = this.apiWrapper.getConfiguration().get('terminal.integrated.shell.windows');
|
||||
terminal.sendText(this.getTextToSendToTerminal(shellType), true);
|
||||
} catch (error) {
|
||||
let message = utils.getErrorMessage(error);
|
||||
this.apiWrapper.showErrorMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
public async doConfigurePython(jupyterInstaller: JupyterServerInstallation): Promise<void> {
|
||||
try {
|
||||
let pythonDialog = new ConfigurePythonDialog(this.appContext, this.outputChannel, jupyterInstaller);
|
||||
await pythonDialog.showDialog();
|
||||
} catch (error) {
|
||||
let message = utils.getErrorMessage(error);
|
||||
this.apiWrapper.showErrorMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
public getTextToSendToTerminal(shellType: any): string {
|
||||
if (utils.getOSPlatform() === utils.Platform.Windows && typeof shellType === 'string') {
|
||||
if (shellType.endsWith('powershell.exe')) {
|
||||
return localizedConstants.msgManagePackagesPowershell;
|
||||
} else if (shellType.endsWith('cmd.exe')) {
|
||||
return localizedConstants.msgManagePackagesCmd;
|
||||
} else {
|
||||
return localizedConstants.msgManagePackagesBash;
|
||||
}
|
||||
} else {
|
||||
return localizedConstants.msgManagePackagesBash;
|
||||
}
|
||||
}
|
||||
|
||||
private getPythonBinDir(): string {
|
||||
return JupyterServerInstallation.getPythonBinPath(this.apiWrapper);
|
||||
}
|
||||
|
||||
public get jupyterInstallation() {
|
||||
return this._jupyterInstallation;
|
||||
}
|
||||
}
|
||||
172
extensions/notebook/src/jupyter/jupyterKernel.ts
Normal file
172
extensions/notebook/src/jupyter/jupyterKernel.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { nb } from 'sqlops';
|
||||
import { Kernel, KernelMessage } from '@jupyterlab/services';
|
||||
|
||||
function toShellMessage(msgImpl: KernelMessage.IShellMessage): nb.IShellMessage {
|
||||
return {
|
||||
channel: msgImpl.channel,
|
||||
type: msgImpl.channel,
|
||||
content: msgImpl.content,
|
||||
header: msgImpl.header,
|
||||
parent_header: msgImpl.parent_header,
|
||||
metadata: msgImpl.metadata
|
||||
};
|
||||
}
|
||||
|
||||
function toStdInMessage(msgImpl: KernelMessage.IStdinMessage): nb.IStdinMessage {
|
||||
return {
|
||||
channel: msgImpl.channel,
|
||||
type: msgImpl.channel,
|
||||
content: msgImpl.content,
|
||||
header: msgImpl.header,
|
||||
parent_header: msgImpl.parent_header,
|
||||
metadata: msgImpl.metadata
|
||||
};
|
||||
}
|
||||
|
||||
function toIOPubMessage(msgImpl: KernelMessage.IIOPubMessage): nb.IIOPubMessage {
|
||||
return {
|
||||
channel: msgImpl.channel,
|
||||
type: msgImpl.channel,
|
||||
content: msgImpl.content,
|
||||
header: msgImpl.header,
|
||||
parent_header: msgImpl.parent_header,
|
||||
metadata: msgImpl.metadata
|
||||
};
|
||||
}
|
||||
|
||||
function toIInputReply(content: nb.IInputReply): KernelMessage.IInputReply {
|
||||
return {
|
||||
value: content.value
|
||||
};
|
||||
}
|
||||
export class JupyterKernel implements nb.IKernel {
|
||||
constructor(private kernelImpl: Kernel.IKernelConnection) {
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.kernelImpl.id;
|
||||
}
|
||||
|
||||
public get name(): string {
|
||||
return this.kernelImpl.name;
|
||||
}
|
||||
|
||||
public get supportsIntellisense(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public get isReady(): boolean {
|
||||
return this.kernelImpl.isReady;
|
||||
}
|
||||
|
||||
public get ready(): Promise<void> {
|
||||
return this.kernelImpl.ready;
|
||||
}
|
||||
|
||||
public get info(): nb.IInfoReply {
|
||||
return this.kernelImpl.info as nb.IInfoReply;
|
||||
}
|
||||
|
||||
public async getSpec(): Promise<nb.IKernelSpec> {
|
||||
let specImpl = await this.kernelImpl.getSpec();
|
||||
return {
|
||||
name: specImpl.name,
|
||||
display_name: specImpl.display_name
|
||||
};
|
||||
}
|
||||
|
||||
requestExecute(content: nb.IExecuteRequest, disposeOnDone?: boolean): nb.IFuture {
|
||||
let futureImpl = this.kernelImpl.requestExecute(content as KernelMessage.IExecuteRequest, disposeOnDone);
|
||||
return new JupyterFuture(futureImpl);
|
||||
}
|
||||
|
||||
requestComplete(content: nb.ICompleteRequest): Promise<nb.ICompleteReplyMsg> {
|
||||
return this.kernelImpl.requestComplete({
|
||||
code: content.code,
|
||||
cursor_pos: content.cursor_pos
|
||||
}).then((completeMsg) => {
|
||||
// Complete msg matches shell message definition, but with clearer content body
|
||||
let msg: nb.ICompleteReplyMsg = toShellMessage(completeMsg);
|
||||
return msg;
|
||||
});
|
||||
}
|
||||
|
||||
interrupt(): Promise<void> {
|
||||
return this.kernelImpl.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
export class JupyterFuture implements nb.IFuture {
|
||||
|
||||
private _inProgress: boolean;
|
||||
|
||||
constructor(private futureImpl: Kernel.IFuture) {
|
||||
this._inProgress = true;
|
||||
}
|
||||
|
||||
public get msg(): nb.IShellMessage {
|
||||
let msgImpl = this.futureImpl.msg;
|
||||
return toShellMessage(msgImpl);
|
||||
}
|
||||
|
||||
public get done(): Promise<nb.IShellMessage> {
|
||||
// Convert on success, leave to throw original error on fail
|
||||
return this.futureImpl.done.then((msgImpl) => {
|
||||
this._inProgress = false;
|
||||
return toShellMessage(msgImpl);
|
||||
});
|
||||
}
|
||||
|
||||
public get inProgress(): boolean {
|
||||
return this._inProgress;
|
||||
}
|
||||
|
||||
public set inProgress(inProg: boolean) {
|
||||
this._inProgress = inProg;
|
||||
}
|
||||
|
||||
setReplyHandler(handler: nb.MessageHandler<nb.IShellMessage>): void {
|
||||
this.futureImpl.onReply = (msg) => {
|
||||
let shellMsg = toShellMessage(msg);
|
||||
return handler.handle(shellMsg);
|
||||
};
|
||||
}
|
||||
|
||||
setStdInHandler(handler: nb.MessageHandler<nb.IStdinMessage>): void {
|
||||
this.futureImpl.onStdin = (msg) => {
|
||||
let shellMsg = toStdInMessage(msg);
|
||||
return handler.handle(shellMsg);
|
||||
};
|
||||
}
|
||||
|
||||
setIOPubHandler(handler: nb.MessageHandler<nb.IIOPubMessage>): void {
|
||||
this.futureImpl.onIOPub = (msg) => {
|
||||
let shellMsg = toIOPubMessage(msg);
|
||||
return handler.handle(shellMsg);
|
||||
};
|
||||
}
|
||||
|
||||
registerMessageHook(hook: (msg: nb.IIOPubMessage) => boolean | PromiseLike<boolean>): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
removeMessageHook(hook: (msg: nb.IIOPubMessage) => boolean | PromiseLike<boolean>): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
sendInputReply(content: nb.IInputReply): void {
|
||||
this.futureImpl.sendInputReply(toIInputReply(content));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.futureImpl.dispose();
|
||||
}
|
||||
}
|
||||
56
extensions/notebook/src/jupyter/jupyterNotebookManager.ts
Normal file
56
extensions/notebook/src/jupyter/jupyterNotebookManager.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { nb } from 'sqlops';
|
||||
import * as vscode from 'vscode';
|
||||
import { ServerConnection, SessionManager } from '@jupyterlab/services';
|
||||
|
||||
import { JupyterSessionManager } from './jupyterSessionManager';
|
||||
import { ApiWrapper } from '../common/apiWrapper';
|
||||
import { LocalJupyterServerManager } from './jupyterServerManager';
|
||||
|
||||
export class JupyterNotebookManager implements nb.NotebookManager, vscode.Disposable {
|
||||
protected _serverSettings: ServerConnection.ISettings;
|
||||
private _sessionManager: JupyterSessionManager;
|
||||
|
||||
constructor(private _serverManager: LocalJupyterServerManager, sessionManager?: JupyterSessionManager, private apiWrapper: ApiWrapper = new ApiWrapper()) {
|
||||
this._sessionManager = sessionManager || new JupyterSessionManager();
|
||||
this._serverManager.onServerStarted(() => {
|
||||
this.setServerSettings(this._serverManager.serverSettings);
|
||||
});
|
||||
|
||||
}
|
||||
public get contentManager(): nb.ContentManager {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public get sessionManager(): nb.SessionManager {
|
||||
return this._sessionManager;
|
||||
}
|
||||
|
||||
public get serverManager(): nb.ServerManager {
|
||||
return this._serverManager;
|
||||
}
|
||||
|
||||
public get serverSettings(): ServerConnection.ISettings {
|
||||
return this._serverSettings;
|
||||
}
|
||||
|
||||
public setServerSettings(settings: Partial<ServerConnection.ISettings>): void {
|
||||
this._serverSettings = ServerConnection.makeSettings(settings);
|
||||
this._sessionManager.setJupyterSessionManager(new SessionManager({ serverSettings: this._serverSettings }));
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this._sessionManager) {
|
||||
this._sessionManager.shutdownAll().then(() => this._sessionManager.dispose());
|
||||
}
|
||||
if (this._serverManager) {
|
||||
this._serverManager.stopServer().then(success => undefined, error => this.apiWrapper.showErrorMessage(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
82
extensions/notebook/src/jupyter/jupyterNotebookProvider.ts
Normal file
82
extensions/notebook/src/jupyter/jupyterNotebookProvider.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { nb } from 'sqlops';
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import * as constants from '../common/constants';
|
||||
import { JupyterNotebookManager } from './jupyterNotebookManager';
|
||||
import { LocalJupyterServerManager } from './jupyterServerManager';
|
||||
|
||||
export type ServerManagerFactory = (documentUri: vscode.Uri) => LocalJupyterServerManager;
|
||||
|
||||
export class JupyterNotebookProvider implements nb.NotebookProvider {
|
||||
readonly providerId: string = constants.jupyterNotebookProviderId;
|
||||
private managerTracker = new Map<string, JupyterNotebookManager>();
|
||||
|
||||
constructor(private createServerManager: ServerManagerFactory) {
|
||||
}
|
||||
|
||||
public getNotebookManager(notebookUri: vscode.Uri): Thenable<nb.NotebookManager> {
|
||||
if (!notebookUri) {
|
||||
return Promise.reject(localize('errNotebookUriMissing', 'A notebook path is required'));
|
||||
}
|
||||
return this.doGetNotebookManager(notebookUri);
|
||||
}
|
||||
|
||||
private doGetNotebookManager(notebookUri: vscode.Uri): Promise<nb.NotebookManager> {
|
||||
let uriString = notebookUri.toString();
|
||||
let manager = this.managerTracker.get(uriString);
|
||||
if (!manager) {
|
||||
let serverManager = this.createServerManager(notebookUri);
|
||||
manager = new JupyterNotebookManager(serverManager);
|
||||
this.managerTracker.set(uriString, manager);
|
||||
}
|
||||
return Promise.resolve(manager);
|
||||
}
|
||||
|
||||
handleNotebookClosed(notebookUri: vscode.Uri): void {
|
||||
if (!notebookUri) {
|
||||
// As this is a notification method, will skip throwing an error here
|
||||
return;
|
||||
}
|
||||
let uriString = notebookUri.toString();
|
||||
let manager = this.managerTracker.get(uriString);
|
||||
if (manager) {
|
||||
this.managerTracker.delete(uriString);
|
||||
manager.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public get standardKernels(): nb.IStandardKernel[] {
|
||||
return [
|
||||
{
|
||||
"name": "Python 3",
|
||||
"connectionProviderIds": []
|
||||
},
|
||||
{
|
||||
"name": "PySpark",
|
||||
"connectionProviderIds": ["HADOOP_KNOX"]
|
||||
},
|
||||
{
|
||||
"name": "PySpark3",
|
||||
"connectionProviderIds": ["HADOOP_KNOX"]
|
||||
},
|
||||
{
|
||||
"name": "Spark | R",
|
||||
"connectionProviderIds": ["HADOOP_KNOX"]
|
||||
},
|
||||
{
|
||||
"name": "Spark | Scala",
|
||||
"connectionProviderIds": ["HADOOP_KNOX"]
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
338
extensions/notebook/src/jupyter/jupyterServerInstallation.ts
Normal file
338
extensions/notebook/src/jupyter/jupyterServerInstallation.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as sqlops from 'sqlops';
|
||||
import { ExecOptions } from 'child_process';
|
||||
import * as decompress from 'decompress';
|
||||
import * as request from 'request';
|
||||
|
||||
import { ApiWrapper } from '../common/apiWrapper';
|
||||
import * as constants from '../common/constants';
|
||||
import * as utils from '../common/utils';
|
||||
import { OutputChannel, ConfigurationTarget, Event, EventEmitter, window } from 'vscode';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
const msgPythonInstallationProgress = localize('msgPythonInstallationProgress', 'Python installation is in progress');
|
||||
const msgPythonInstallationComplete = localize('msgPythonInstallationComplete', 'Python installation is complete');
|
||||
const msgPythonDownloadError = localize('msgPythonDownloadError', 'Error while downloading python setup');
|
||||
const msgPythonDownloadPending = localize('msgPythonDownloadPending', 'Downloading python package');
|
||||
const msgPythonUnpackPending = localize('msgPythonUnpackPending', 'Unpacking python package');
|
||||
const msgPythonDirectoryError = localize('msgPythonDirectoryError', 'Error while creating python installation directory');
|
||||
const msgPythonUnpackError = localize('msgPythonUnpackError', 'Error while unpacking python bundle');
|
||||
const msgTaskName = localize('msgTaskName', 'Installing Notebook dependencies');
|
||||
const msgInstallPkgStart = localize('msgInstallPkgStart', 'Installing Notebook dependencies, see Tasks view for more information');
|
||||
const msgInstallPkgFinish = localize('msgInstallPkgFinish', 'Notebook dependencies installation is complete');
|
||||
function msgDependenciesInstallationFailed(errorMessage: string): string { return localize('msgDependenciesInstallationFailed', 'Installing Notebook dependencies failed with error: {0}', errorMessage); }
|
||||
function msgDownloadPython(platform: string, pythonDownloadUrl: string): string { return localize('msgDownloadPython', 'Downloading local python for platform: {0} to {1}', platform, pythonDownloadUrl); }
|
||||
|
||||
export default class JupyterServerInstallation {
|
||||
/**
|
||||
* Path to the folder where all configuration sets will be stored. Should always be:
|
||||
* %extension_path%/jupyter_config
|
||||
*/
|
||||
public apiWrapper: ApiWrapper;
|
||||
public extensionPath: string;
|
||||
public pythonBinPath: string;
|
||||
public outputChannel: OutputChannel;
|
||||
public configRoot: string;
|
||||
public pythonEnvVarPath: string;
|
||||
public execOptions: ExecOptions;
|
||||
|
||||
private _pythonInstallationPath: string;
|
||||
private _pythonExecutable: string;
|
||||
|
||||
// Allows dependencies to be installed even if an existing installation is already present
|
||||
private _forceInstall: boolean;
|
||||
|
||||
private static readonly DefaultPythonLocation = path.join(utils.getUserHome(), 'azuredatastudio-python');
|
||||
|
||||
private _installCompleteEmitter = new EventEmitter<string>();
|
||||
|
||||
constructor(extensionPath: string, outputChannel: OutputChannel, apiWrapper: ApiWrapper, pythonInstallationPath?: string, forceInstall?: boolean) {
|
||||
this.extensionPath = extensionPath;
|
||||
this.outputChannel = outputChannel;
|
||||
this.apiWrapper = apiWrapper;
|
||||
this._pythonInstallationPath = pythonInstallationPath || JupyterServerInstallation.getPythonInstallPath(this.apiWrapper);
|
||||
this.configRoot = path.join(this.extensionPath, constants.jupyterConfigRootFolder);
|
||||
this._forceInstall = !!forceInstall;
|
||||
|
||||
this.configurePackagePaths();
|
||||
}
|
||||
|
||||
public get onInstallComplete(): Event<string> {
|
||||
return this._installCompleteEmitter.event;
|
||||
}
|
||||
|
||||
public static async getInstallation(
|
||||
extensionPath: string,
|
||||
outputChannel: OutputChannel,
|
||||
apiWrapper: ApiWrapper,
|
||||
pythonInstallationPath?: string,
|
||||
forceInstall?: boolean): Promise<JupyterServerInstallation> {
|
||||
|
||||
let installation = new JupyterServerInstallation(extensionPath, outputChannel, apiWrapper, pythonInstallationPath, forceInstall);
|
||||
await installation.startInstallProcess();
|
||||
|
||||
return installation;
|
||||
}
|
||||
|
||||
private async installDependencies(backgroundOperation: sqlops.BackgroundOperation): Promise<void> {
|
||||
if (!fs.existsSync(this._pythonExecutable) || this._forceInstall) {
|
||||
window.showInformationMessage(msgInstallPkgStart);
|
||||
this.outputChannel.show(true);
|
||||
this.outputChannel.appendLine(msgPythonInstallationProgress);
|
||||
backgroundOperation.updateStatus(sqlops.TaskStatus.InProgress, msgPythonInstallationProgress);
|
||||
await this.installPythonPackage(backgroundOperation);
|
||||
backgroundOperation.updateStatus(sqlops.TaskStatus.InProgress, msgPythonInstallationComplete);
|
||||
this.outputChannel.appendLine(msgPythonInstallationComplete);
|
||||
|
||||
// Install jupyter on Windows because local python is not bundled with jupyter unlike linux and MacOS.
|
||||
await this.installJupyterProsePackage();
|
||||
await this.installSparkMagic();
|
||||
backgroundOperation.updateStatus(sqlops.TaskStatus.Succeeded, msgInstallPkgFinish);
|
||||
window.showInformationMessage(msgInstallPkgFinish);
|
||||
}
|
||||
}
|
||||
|
||||
private installPythonPackage(backgroundOperation: sqlops.BackgroundOperation): Promise<void> {
|
||||
let bundleVersion = constants.pythonBundleVersion;
|
||||
let pythonVersion = constants.pythonVersion;
|
||||
let packageName = 'python-#pythonversion-#platform-#bundleversion.#extension';
|
||||
let platformId = utils.getOSPlatformId();
|
||||
|
||||
packageName = packageName.replace('#platform', platformId)
|
||||
.replace('#pythonversion', pythonVersion)
|
||||
.replace('#bundleversion', bundleVersion)
|
||||
.replace('#extension', process.platform === constants.winPlatform ? 'zip' : 'tar.gz');
|
||||
|
||||
let pythonDownloadUrl = undefined;
|
||||
switch (utils.getOSPlatform()) {
|
||||
case utils.Platform.Windows:
|
||||
pythonDownloadUrl = 'https://go.microsoft.com/fwlink/?linkid=2065977';
|
||||
break;
|
||||
case utils.Platform.Mac:
|
||||
pythonDownloadUrl = 'https://go.microsoft.com/fwlink/?linkid=2065976';
|
||||
break;
|
||||
default:
|
||||
// Default to linux
|
||||
pythonDownloadUrl = 'https://go.microsoft.com/fwlink/?linkid=2065975';
|
||||
break;
|
||||
}
|
||||
|
||||
let pythonPackagePathLocal = this._pythonInstallationPath + '/' + packageName;
|
||||
let self = undefined;
|
||||
return new Promise((resolve, reject) => {
|
||||
self = this;
|
||||
backgroundOperation.updateStatus(sqlops.TaskStatus.InProgress, msgDownloadPython(platformId, pythonDownloadUrl));
|
||||
fs.mkdirs(this._pythonInstallationPath, (err) => {
|
||||
if (err) {
|
||||
backgroundOperation.updateStatus(sqlops.TaskStatus.InProgress, msgPythonDirectoryError);
|
||||
reject(err);
|
||||
}
|
||||
|
||||
let totalMegaBytes: number = undefined;
|
||||
let receivedBytes = 0;
|
||||
let printThreshold = 0.1;
|
||||
request.get(pythonDownloadUrl, { timeout: 20000 })
|
||||
.on('error', (downloadError) => {
|
||||
backgroundOperation.updateStatus(sqlops.TaskStatus.InProgress, msgPythonDownloadError);
|
||||
reject(downloadError);
|
||||
})
|
||||
.on('response', (response) => {
|
||||
if (response.statusCode !== 200) {
|
||||
backgroundOperation.updateStatus(sqlops.TaskStatus.InProgress, msgPythonDownloadError);
|
||||
reject(response.statusMessage);
|
||||
}
|
||||
|
||||
let totalBytes = parseInt(response.headers['content-length']);
|
||||
totalMegaBytes = totalBytes / (1024 * 1024);
|
||||
this.outputChannel.appendLine(`${msgPythonDownloadPending} (0 / ${totalMegaBytes.toFixed(2)} MB)`);
|
||||
})
|
||||
.on('data', (data) => {
|
||||
receivedBytes += data.length;
|
||||
if (totalMegaBytes) {
|
||||
let receivedMegaBytes = receivedBytes / (1024 * 1024);
|
||||
let percentage = receivedMegaBytes / totalMegaBytes;
|
||||
if (percentage >= printThreshold) {
|
||||
this.outputChannel.appendLine(`${msgPythonDownloadPending} (${receivedMegaBytes.toFixed(2)} / ${totalMegaBytes.toFixed(2)} MB)`);
|
||||
printThreshold += 0.1;
|
||||
}
|
||||
}
|
||||
})
|
||||
.pipe(fs.createWriteStream(pythonPackagePathLocal))
|
||||
.on('close', () => {
|
||||
//unpack python zip/tar file
|
||||
this.outputChannel.appendLine(msgPythonUnpackPending);
|
||||
let pythonSourcePath = path.join(this._pythonInstallationPath, constants.pythonBundleVersion);
|
||||
if (fs.existsSync(pythonSourcePath)) {
|
||||
try {
|
||||
fs.removeSync(pythonSourcePath);
|
||||
} catch (err) {
|
||||
backgroundOperation.updateStatus(sqlops.TaskStatus.InProgress, msgPythonUnpackError);
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
decompress(pythonPackagePathLocal, self._pythonInstallationPath).then(files => {
|
||||
//Delete zip/tar file
|
||||
fs.unlink(pythonPackagePathLocal, (err) => {
|
||||
if (err) {
|
||||
backgroundOperation.updateStatus(sqlops.TaskStatus.InProgress, msgPythonUnpackError);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
resolve();
|
||||
}).catch(err => {
|
||||
backgroundOperation.updateStatus(sqlops.TaskStatus.InProgress, msgPythonUnpackError);
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
.on('error', (downloadError) => {
|
||||
backgroundOperation.updateStatus(sqlops.TaskStatus.InProgress, msgPythonDownloadError);
|
||||
reject(downloadError);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private configurePackagePaths(): void {
|
||||
//Python source path up to bundle version
|
||||
let pythonSourcePath = path.join(this._pythonInstallationPath, constants.pythonBundleVersion);
|
||||
|
||||
// Update python paths and properties to reference user's local python.
|
||||
let pythonBinPathSuffix = process.platform === constants.winPlatform ? '' : 'bin';
|
||||
|
||||
this._pythonExecutable = path.join(pythonSourcePath, process.platform === constants.winPlatform ? 'python.exe' : 'bin/python3');
|
||||
this.pythonBinPath = path.join(pythonSourcePath, pythonBinPathSuffix);
|
||||
|
||||
// Store paths to python libraries required to run jupyter.
|
||||
this.pythonEnvVarPath = process.env.Path;
|
||||
|
||||
let delimiter = path.delimiter;
|
||||
if (process.platform === constants.winPlatform) {
|
||||
let pythonScriptsPath = path.join(pythonSourcePath, 'Scripts');
|
||||
this.pythonEnvVarPath = pythonScriptsPath + delimiter + this.pythonEnvVarPath;
|
||||
}
|
||||
this.pythonEnvVarPath = this.pythonBinPath + delimiter + this.pythonEnvVarPath;
|
||||
|
||||
// Store the executable options to run child processes with env var without interfering parent env var.
|
||||
let env = Object.assign({}, process.env);
|
||||
env['PATH'] = this.pythonEnvVarPath;
|
||||
this.execOptions = {
|
||||
env: env
|
||||
};
|
||||
}
|
||||
|
||||
public async startInstallProcess(pythonInstallationPath?: string): Promise<void> {
|
||||
if (pythonInstallationPath) {
|
||||
this._pythonInstallationPath = pythonInstallationPath;
|
||||
this.configurePackagePaths();
|
||||
}
|
||||
let updateConfig = () => {
|
||||
let notebookConfig = this.apiWrapper.getConfiguration(constants.notebookConfigKey);
|
||||
notebookConfig.update(constants.pythonPathConfigKey, this._pythonInstallationPath, ConfigurationTarget.Global);
|
||||
};
|
||||
if (!fs.existsSync(this._pythonExecutable) || this._forceInstall) {
|
||||
this.apiWrapper.startBackgroundOperation({
|
||||
displayName: msgTaskName,
|
||||
description: msgTaskName,
|
||||
isCancelable: false,
|
||||
operation: op => {
|
||||
this.installDependencies(op)
|
||||
.then(() => {
|
||||
this._installCompleteEmitter.fire();
|
||||
updateConfig();
|
||||
})
|
||||
.catch(err => {
|
||||
let errorMsg = msgDependenciesInstallationFailed(err);
|
||||
op.updateStatus(sqlops.TaskStatus.Failed, errorMsg);
|
||||
this.apiWrapper.showErrorMessage(errorMsg);
|
||||
this._installCompleteEmitter.fire(errorMsg);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Python executable already exists, but the path setting wasn't defined,
|
||||
// so update it here
|
||||
this._installCompleteEmitter.fire();
|
||||
updateConfig();
|
||||
}
|
||||
}
|
||||
|
||||
private async installJupyterProsePackage(): Promise<void> {
|
||||
if (process.platform === constants.winPlatform) {
|
||||
let installJupyterCommand = `${this._pythonExecutable} -m pip install pandas==0.22.0 jupyter prose-codeaccelerator==1.3.0 --extra-index-url https://prose-python-packages.azurewebsites.net --no-warn-script-location`;
|
||||
this.outputChannel.show(true);
|
||||
this.outputChannel.appendLine(localize('msgInstallStart', 'Installing required packages to run Notebooks...'));
|
||||
await utils.executeStreamedCommand(installJupyterCommand, this.outputChannel);
|
||||
this.outputChannel.appendLine(localize('msgJupyterInstallDone', '... Jupyter installation complete.'));
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
private async installSparkMagic(): Promise<void> {
|
||||
if (process.platform === constants.winPlatform) {
|
||||
let sparkMagicPath = path.join(this.extensionPath, 'wheels/sparkmagic-#sparkMagicVersion-py3-none-any.whl'.replace('#sparkMagicVersion', constants.sparkMagicVersion));
|
||||
let installSparkMagic = `${this._pythonExecutable} -m pip install ${sparkMagicPath} --no-warn-script-location`;
|
||||
this.outputChannel.show(true);
|
||||
this.outputChannel.appendLine(localize('msgInstallingSpark', 'Installing SparkMagic...'));
|
||||
await utils.executeStreamedCommand(installSparkMagic, this.outputChannel);
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
public get pythonExecutable(): string {
|
||||
return this._pythonExecutable;
|
||||
}
|
||||
|
||||
public static isPythonInstalled(apiWrapper: ApiWrapper): boolean {
|
||||
// Don't use _pythonExecutable here, since it could be populated with a default value
|
||||
let pathSetting = JupyterServerInstallation.getPythonPathSetting(apiWrapper);
|
||||
if (!pathSetting) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let pythonExe = path.join(
|
||||
pathSetting,
|
||||
constants.pythonBundleVersion,
|
||||
process.platform === constants.winPlatform ? 'python.exe' : 'bin/python3');
|
||||
return fs.existsSync(pythonExe);
|
||||
}
|
||||
|
||||
public static getPythonInstallPath(apiWrapper: ApiWrapper): string {
|
||||
let userPath = JupyterServerInstallation.getPythonPathSetting(apiWrapper);
|
||||
return userPath ? userPath : JupyterServerInstallation.DefaultPythonLocation;
|
||||
}
|
||||
|
||||
private static getPythonPathSetting(apiWrapper: ApiWrapper): string {
|
||||
let path = undefined;
|
||||
if (apiWrapper) {
|
||||
let notebookConfig = apiWrapper.getConfiguration(constants.notebookConfigKey);
|
||||
if (notebookConfig) {
|
||||
let configPythonPath = notebookConfig[constants.pythonPathConfigKey];
|
||||
if (configPythonPath && fs.existsSync(configPythonPath)) {
|
||||
path = configPythonPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
public static getPythonBinPath(apiWrapper: ApiWrapper): string {
|
||||
let pythonBinPathSuffix = process.platform === constants.winPlatform ? '' : 'bin';
|
||||
|
||||
return path.join(
|
||||
JupyterServerInstallation.getPythonInstallPath(apiWrapper),
|
||||
constants.pythonBundleVersion,
|
||||
pythonBinPathSuffix);
|
||||
}
|
||||
}
|
||||
147
extensions/notebook/src/jupyter/jupyterServerManager.ts
Normal file
147
extensions/notebook/src/jupyter/jupyterServerManager.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { nb } from 'sqlops';
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { ServerConnection } from '@jupyterlab/services';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import { ApiWrapper } from '../common/apiWrapper';
|
||||
import JupyterServerInstallation from './jupyterServerInstallation';
|
||||
import * as utils from '../common/utils';
|
||||
import { IServerInstance } from './common';
|
||||
import { PerNotebookServerInstance, IInstanceOptions } from './serverInstance';
|
||||
|
||||
export interface IServerManagerOptions {
|
||||
documentPath: string;
|
||||
jupyterInstallation: Promise<JupyterServerInstallation>;
|
||||
extensionContext: vscode.ExtensionContext;
|
||||
apiWrapper?: ApiWrapper;
|
||||
factory?: ServerInstanceFactory;
|
||||
}
|
||||
export class LocalJupyterServerManager implements nb.ServerManager, vscode.Disposable {
|
||||
private _serverSettings: Partial<ServerConnection.ISettings>;
|
||||
private _onServerStarted = new vscode.EventEmitter<void>();
|
||||
private _instanceOptions: IInstanceOptions;
|
||||
private apiWrapper: ApiWrapper;
|
||||
private jupyterServer: IServerInstance;
|
||||
factory: ServerInstanceFactory;
|
||||
constructor(private options: IServerManagerOptions) {
|
||||
this.apiWrapper = options.apiWrapper || new ApiWrapper();
|
||||
this.factory = options.factory || new ServerInstanceFactory();
|
||||
}
|
||||
|
||||
public get serverSettings(): Partial<ServerConnection.ISettings> {
|
||||
return this._serverSettings;
|
||||
}
|
||||
|
||||
public get isStarted(): boolean {
|
||||
return !!this.jupyterServer;
|
||||
}
|
||||
|
||||
public get instanceOptions(): IInstanceOptions {
|
||||
return this._instanceOptions;
|
||||
}
|
||||
|
||||
public get onServerStarted(): vscode.Event<void> {
|
||||
return this._onServerStarted.event;
|
||||
}
|
||||
|
||||
public async startServer(): Promise<void> {
|
||||
try {
|
||||
this.jupyterServer = await this.doStartServer();
|
||||
this.options.extensionContext.subscriptions.push(this);
|
||||
let partialSettings = LocalJupyterServerManager.getLocalConnectionSettings(this.jupyterServer.uri);
|
||||
this._serverSettings = partialSettings;
|
||||
this._onServerStarted.fire();
|
||||
|
||||
} catch (error) {
|
||||
this.apiWrapper.showErrorMessage(localize('startServerFailed', 'Starting local Notebook server failed with error {0}', utils.getErrorMessage(error)));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.stopServer().catch(err => {
|
||||
let msg = utils.getErrorMessage(err);
|
||||
this.apiWrapper.showErrorMessage(localize('shutdownError', 'Shutdown of Notebook server failed: {0}', msg));
|
||||
});
|
||||
}
|
||||
|
||||
public async stopServer(): Promise<void> {
|
||||
if (this.jupyterServer) {
|
||||
await this.jupyterServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public static getLocalConnectionSettings(uri: vscode.Uri): Partial<ServerConnection.ISettings> {
|
||||
return {
|
||||
baseUrl: `${uri.scheme}://${uri.authority}`,
|
||||
token: LocalJupyterServerManager.getToken(uri.query)
|
||||
};
|
||||
}
|
||||
|
||||
private static getToken(query: string): string {
|
||||
if (query) {
|
||||
let parts = query.split('=');
|
||||
if (parts && parts.length >= 2) {
|
||||
return parts[1];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private get documentPath(): string {
|
||||
return this.options.documentPath;
|
||||
}
|
||||
|
||||
private async doStartServer(): Promise<IServerInstance> { // We can't find or create servers until the installation is complete
|
||||
let installation = await this.options.jupyterInstallation;
|
||||
|
||||
// Calculate the path to use as the notebook-dir for Jupyter based on the path of the uri of the
|
||||
// notebook to open. This will be the workspace folder if the notebook uri is inside a workspace
|
||||
// folder. Otherwise, it will be the folder that the notebook is inside. Ultimately, this means
|
||||
// a new notebook server will be started for each folder a notebook is opened from.
|
||||
//
|
||||
// eg, opening:
|
||||
// /path1/nb1.ipynb
|
||||
// /path2/nb2.ipynb
|
||||
// /path2/nb3.ipynb
|
||||
// ... will result in 2 notebook servers being started, one for /path1/ and one for /path2/
|
||||
let notebookDir = this.apiWrapper.getWorkspacePathFromUri(vscode.Uri.file(this.documentPath));
|
||||
notebookDir = notebookDir || path.dirname(this.documentPath);
|
||||
|
||||
// TODO handle notification of start/stop status
|
||||
// notebookContext.updateLoadingMessage(localizedConstants.msgJupyterStarting);
|
||||
|
||||
// TODO refactor server instance so it doesn't need the kernel. Likely need to reimplement this
|
||||
// for notebook version
|
||||
let serverInstanceOptions: IInstanceOptions = {
|
||||
documentPath: this.documentPath,
|
||||
notebookDirectory: notebookDir,
|
||||
install: installation
|
||||
};
|
||||
|
||||
this._instanceOptions = serverInstanceOptions;
|
||||
|
||||
let server = this.factory.createInstance(serverInstanceOptions);
|
||||
await server.configure();
|
||||
await server.start();
|
||||
|
||||
return server;
|
||||
}
|
||||
}
|
||||
|
||||
export class ServerInstanceFactory {
|
||||
|
||||
createInstance(options: IInstanceOptions): IServerInstance {
|
||||
return new PerNotebookServerInstance(options);
|
||||
}
|
||||
}
|
||||
|
||||
338
extensions/notebook/src/jupyter/jupyterSessionManager.ts
Normal file
338
extensions/notebook/src/jupyter/jupyterSessionManager.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { nb, ServerInfo, connection, IConnectionProfile } from 'sqlops';
|
||||
import { Session, Kernel } from '@jupyterlab/services';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { Uri } from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as utils from '../common/utils';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import { JupyterKernel } from './jupyterKernel';
|
||||
import { Deferred } from '../common/promise';
|
||||
|
||||
const configBase = {
|
||||
'kernel_python_credentials': {
|
||||
'url': ''
|
||||
},
|
||||
'kernel_scala_credentials': {
|
||||
'url': ''
|
||||
},
|
||||
'kernel_r_credentials': {
|
||||
'url': ''
|
||||
},
|
||||
|
||||
'ignore_ssl_errors': true,
|
||||
|
||||
'logging_config': {
|
||||
'version': 1,
|
||||
'formatters': {
|
||||
'magicsFormatter': {
|
||||
'format': '%(asctime)s\t%(levelname)s\t%(message)s',
|
||||
'datefmt': ''
|
||||
}
|
||||
},
|
||||
'handlers': {
|
||||
'magicsHandler': {
|
||||
'class': 'hdijupyterutils.filehandler.MagicsFileHandler',
|
||||
'formatter': 'magicsFormatter',
|
||||
'home_path': ''
|
||||
}
|
||||
},
|
||||
'loggers': {
|
||||
'magicsLogger': {
|
||||
'handlers': ['magicsHandler'],
|
||||
'level': 'DEBUG',
|
||||
'propagate': 0
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const KNOX_ENDPOINT_SERVER = 'host';
|
||||
const KNOX_ENDPOINT_PORT = 'knoxport';
|
||||
const KNOX_ENDPOINT = 'knox';
|
||||
const SQL_PROVIDER = 'MSSQL';
|
||||
const USER = 'user';
|
||||
const DEFAULT_CLUSTER_USER_NAME = 'root';
|
||||
|
||||
export class JupyterSessionManager implements nb.SessionManager {
|
||||
private _ready: Deferred<void>;
|
||||
private _isReady: boolean;
|
||||
private _sessionManager: Session.IManager;
|
||||
private static _sessions: JupyterSession[] = [];
|
||||
|
||||
constructor() {
|
||||
this._isReady = false;
|
||||
this._ready = new Deferred<void>();
|
||||
}
|
||||
|
||||
public setJupyterSessionManager(sessionManager: Session.IManager): void {
|
||||
this._sessionManager = sessionManager;
|
||||
sessionManager.ready
|
||||
.then(() => {
|
||||
this._isReady = true;
|
||||
this._ready.resolve();
|
||||
}).catch((error) => {
|
||||
this._isReady = false;
|
||||
this._ready.reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
public get isReady(): boolean {
|
||||
return this._isReady;
|
||||
}
|
||||
public get ready(): Promise<void> {
|
||||
return this._ready.promise;
|
||||
}
|
||||
|
||||
public get specs(): nb.IAllKernels | undefined {
|
||||
if (!this._isReady) {
|
||||
return undefined;
|
||||
}
|
||||
let specs = this._sessionManager.specs;
|
||||
if (!specs) {
|
||||
return undefined;
|
||||
}
|
||||
let kernels: nb.IKernelSpec[] = Object.keys(specs.kernelspecs).map(k => {
|
||||
let value = specs.kernelspecs[k];
|
||||
let kernel: nb.IKernelSpec = {
|
||||
name: k,
|
||||
display_name: value.display_name ? value.display_name : k
|
||||
};
|
||||
// TODO add more info to kernels
|
||||
return kernel;
|
||||
});
|
||||
let allKernels: nb.IAllKernels = {
|
||||
defaultKernel: specs.default,
|
||||
kernels: kernels
|
||||
};
|
||||
return allKernels;
|
||||
}
|
||||
|
||||
public async startNew(options: nb.ISessionOptions): Promise<nb.ISession> {
|
||||
if (!this._isReady) {
|
||||
// no-op
|
||||
return Promise.reject(new Error(localize('errorStartBeforeReady', 'Cannot start a session, the manager is not yet initialized')));
|
||||
}
|
||||
let sessionImpl = await this._sessionManager.startNew(options);
|
||||
let jupyterSession = new JupyterSession(sessionImpl);
|
||||
let index = JupyterSessionManager._sessions.findIndex(session => session.path === options.path);
|
||||
if (index > -1) {
|
||||
JupyterSessionManager._sessions.splice(index);
|
||||
}
|
||||
JupyterSessionManager._sessions.push(jupyterSession);
|
||||
return jupyterSession;
|
||||
}
|
||||
|
||||
public listRunning(): JupyterSession[] {
|
||||
return JupyterSessionManager._sessions;
|
||||
}
|
||||
|
||||
public shutdown(id: string): Promise<void> {
|
||||
if (!this._isReady) {
|
||||
// no-op
|
||||
return Promise.resolve();
|
||||
}
|
||||
let index = JupyterSessionManager._sessions.findIndex(session => session.id === id);
|
||||
if (index > -1) {
|
||||
JupyterSessionManager._sessions.splice(index);
|
||||
}
|
||||
if (this._sessionManager && !this._sessionManager.isDisposed) {
|
||||
return this._sessionManager.shutdown(id);
|
||||
}
|
||||
}
|
||||
|
||||
public shutdownAll(): Promise<void> {
|
||||
return this._sessionManager.shutdownAll();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._sessionManager.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export class JupyterSession implements nb.ISession {
|
||||
private _kernel: nb.IKernel;
|
||||
|
||||
constructor(private sessionImpl: Session.ISession) {
|
||||
}
|
||||
|
||||
public get canChangeKernels(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.sessionImpl.id;
|
||||
}
|
||||
|
||||
public get path(): string {
|
||||
return this.sessionImpl.path;
|
||||
}
|
||||
|
||||
public get name(): string {
|
||||
return this.sessionImpl.name;
|
||||
}
|
||||
|
||||
public get type(): string {
|
||||
return this.sessionImpl.type;
|
||||
}
|
||||
|
||||
public get status(): nb.KernelStatus {
|
||||
return this.sessionImpl.status;
|
||||
}
|
||||
|
||||
public get kernel(): nb.IKernel {
|
||||
if (!this._kernel) {
|
||||
let kernelImpl = this.sessionImpl.kernel;
|
||||
if (kernelImpl) {
|
||||
this._kernel = new JupyterKernel(kernelImpl);
|
||||
}
|
||||
}
|
||||
return this._kernel;
|
||||
}
|
||||
|
||||
public async changeKernel(kernelInfo: nb.IKernelSpec): Promise<nb.IKernel> {
|
||||
// For now, Jupyter implementation handles disposal etc. so we can just
|
||||
// null out our kernel and let the changeKernel call handle this
|
||||
this._kernel = undefined;
|
||||
// For now, just using name. It's unclear how we'd know the ID
|
||||
let options: Partial<Kernel.IModel> = {
|
||||
name: kernelInfo.name
|
||||
};
|
||||
return this.sessionImpl.changeKernel(options).then((kernelImpl) => {
|
||||
this._kernel = new JupyterKernel(kernelImpl);
|
||||
return this._kernel;
|
||||
});
|
||||
}
|
||||
|
||||
public async configureKernel(): Promise<void> {
|
||||
let sparkmagicConfDir = path.join(utils.getUserHome(), '.sparkmagic');
|
||||
await utils.mkDir(sparkmagicConfDir);
|
||||
|
||||
// Default to localhost in config file.
|
||||
let creds: ICredentials = {
|
||||
'url': 'http://localhost:8088'
|
||||
};
|
||||
|
||||
let config: ISparkMagicConfig = Object.assign({}, configBase);
|
||||
this.updateConfig(config, creds, sparkmagicConfDir);
|
||||
|
||||
let configFilePath = path.join(sparkmagicConfDir, 'config.json');
|
||||
await fs.writeFile(configFilePath, JSON.stringify(config));
|
||||
}
|
||||
|
||||
public async configureConnection(connection: IConnectionProfile): Promise<void> {
|
||||
if (connection && connection.providerName && this.isSparkKernel(this.sessionImpl.kernel.name)) {
|
||||
// TODO may need to reenable a way to get the credential
|
||||
// await this._connection.getCredential();
|
||||
// %_do_not_call_change_endpoint is a SparkMagic command that lets users change endpoint options,
|
||||
// such as user/profile/host name/auth type
|
||||
|
||||
//Update server info with bigdata endpoint - Unified Connection
|
||||
if (connection.providerName === SQL_PROVIDER) {
|
||||
let clusterEndpoint: IEndpoint = await this.getClusterEndpoint(connection.id, KNOX_ENDPOINT);
|
||||
if (!clusterEndpoint) {
|
||||
let kernelDisplayName: string = await this.getKernelDisplayName();
|
||||
return Promise.reject(new Error(localize('connectionNotValid', 'Spark kernels require a connection to a SQL Server big data cluster master instance.')));
|
||||
}
|
||||
connection.options[KNOX_ENDPOINT_SERVER] = clusterEndpoint.ipAddress;
|
||||
connection.options[KNOX_ENDPOINT_PORT] = clusterEndpoint.port;
|
||||
connection.options[USER] = DEFAULT_CLUSTER_USER_NAME;
|
||||
}
|
||||
else {
|
||||
connection.options[KNOX_ENDPOINT_PORT] = this.getKnoxPortOrDefault(connection);
|
||||
}
|
||||
this.setHostAndPort(':', connection);
|
||||
this.setHostAndPort(',', connection);
|
||||
|
||||
let server = Uri.parse(utils.getLivyUrl(connection.options[KNOX_ENDPOINT_SERVER], connection.options[KNOX_ENDPOINT_PORT])).toString();
|
||||
let doNotCallChangeEndpointParams =
|
||||
`%_do_not_call_change_endpoint --username=${connection.options[USER]} --password=${connection.options['password']} --server=${server} --auth=Basic_Access`;
|
||||
let future = this.sessionImpl.kernel.requestExecute({
|
||||
code: doNotCallChangeEndpointParams
|
||||
}, true);
|
||||
await future.done;
|
||||
}
|
||||
}
|
||||
|
||||
private async getKernelDisplayName(): Promise<string> {
|
||||
let spec = await this.kernel.getSpec();
|
||||
return spec.display_name;
|
||||
}
|
||||
|
||||
private isSparkKernel(kernelName: string): boolean {
|
||||
return kernelName && kernelName.toLowerCase().indexOf('spark') > -1;
|
||||
}
|
||||
|
||||
private setHostAndPort(delimeter: string, connection: IConnectionProfile): void {
|
||||
let originalHost = connection.options[KNOX_ENDPOINT_SERVER];
|
||||
if (!originalHost) {
|
||||
return;
|
||||
}
|
||||
let index = originalHost.indexOf(delimeter);
|
||||
if (index > -1) {
|
||||
connection.options[KNOX_ENDPOINT_SERVER] = originalHost.slice(0, index);
|
||||
connection.options[KNOX_ENDPOINT_PORT] = originalHost.slice(index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private updateConfig(config: ISparkMagicConfig, creds: ICredentials, homePath: string): void {
|
||||
config.kernel_python_credentials = creds;
|
||||
config.kernel_scala_credentials = creds;
|
||||
config.kernel_r_credentials = creds;
|
||||
config.logging_config.handlers.magicsHandler.home_path = homePath;
|
||||
}
|
||||
|
||||
private getKnoxPortOrDefault(connectionProfile: IConnectionProfile): string {
|
||||
let port = connectionProfile.options[KNOX_ENDPOINT_PORT];
|
||||
if (!port) {
|
||||
port = '30443';
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
private async getClusterEndpoint(profileId: string, serviceName: string): Promise<IEndpoint> {
|
||||
let serverInfo: ServerInfo = await connection.getServerInfo(profileId);
|
||||
if (!serverInfo || !serverInfo.options) {
|
||||
return undefined;
|
||||
}
|
||||
let endpoints: IEndpoint[] = serverInfo.options['clusterEndpoints'];
|
||||
if (!endpoints || endpoints.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return endpoints.find(ep => ep.serviceName.toLowerCase() === serviceName.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
interface ICredentials {
|
||||
'url': string;
|
||||
}
|
||||
|
||||
interface IEndpoint {
|
||||
serviceName: string;
|
||||
ipAddress: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
interface ISparkMagicConfig {
|
||||
kernel_python_credentials: ICredentials;
|
||||
kernel_scala_credentials: ICredentials;
|
||||
kernel_r_credentials: ICredentials;
|
||||
ignore_ssl_errors?: boolean;
|
||||
logging_config: {
|
||||
handlers: {
|
||||
magicsHandler: {
|
||||
home_path: string;
|
||||
class?: string;
|
||||
formatter?: string
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
76
extensions/notebook/src/jupyter/jupyterSettingWriter.ts
Normal file
76
extensions/notebook/src/jupyter/jupyterSettingWriter.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as fs from 'fs-extra';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import * as constants from '../common/constants';
|
||||
|
||||
export enum SettingType {
|
||||
String,
|
||||
Number,
|
||||
Boolean,
|
||||
Set
|
||||
}
|
||||
export class ISetting {
|
||||
key: string;
|
||||
value: string | number | boolean;
|
||||
type: SettingType;
|
||||
}
|
||||
|
||||
export class JupyterSettingWriter {
|
||||
private settings: ISetting[] = [];
|
||||
|
||||
constructor(private baseFile: string) {
|
||||
}
|
||||
|
||||
public addSetting(setting: ISetting): void {
|
||||
this.settings.push(setting);
|
||||
}
|
||||
|
||||
public async writeSettings(targetFile: string): Promise<void> {
|
||||
let settings = await this.printSettings();
|
||||
await fs.writeFile(targetFile, settings);
|
||||
}
|
||||
|
||||
public async printSettings(): Promise<string> {
|
||||
let content = '';
|
||||
let newLine = process.platform === constants.winPlatform ? '\r\n' : '\n';
|
||||
if (this.baseFile) {
|
||||
let sourceContents = await fs.readFile(this.baseFile);
|
||||
content += sourceContents.toString();
|
||||
}
|
||||
|
||||
for (let setting of this.settings) {
|
||||
content += newLine;
|
||||
content += this.printSetting(setting);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
private printSetting(setting: ISetting): string {
|
||||
let value: string;
|
||||
switch (setting.type) {
|
||||
case SettingType.Boolean:
|
||||
value = setting.value ? 'True' : 'False';
|
||||
break;
|
||||
case SettingType.String:
|
||||
value = `'${setting.value}'`;
|
||||
break;
|
||||
case SettingType.Number:
|
||||
value = `${setting.value}`;
|
||||
break;
|
||||
case SettingType.Set:
|
||||
value = `set([${setting.value}])`;
|
||||
break;
|
||||
default:
|
||||
throw new Error(localize('UnexpectedSettingType', 'Unexpected setting type {0}', setting.type));
|
||||
}
|
||||
return `c.${setting.key} = ${value}`;
|
||||
}
|
||||
}
|
||||
43
extensions/notebook/src/jupyter/remoteContentManager.ts
Normal file
43
extensions/notebook/src/jupyter/remoteContentManager.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { nb } from 'sqlops';
|
||||
import * as vscode from 'vscode';
|
||||
import { Contents } from '@jupyterlab/services';
|
||||
|
||||
export class RemoteContentManager implements nb.ContentManager {
|
||||
|
||||
constructor(private contents: Contents.IManager) {
|
||||
}
|
||||
|
||||
public getNotebookContents(notebookUri: vscode.Uri): Thenable<nb.INotebookContents> {
|
||||
return this.getNotebookContentsAsync(notebookUri.fsPath);
|
||||
}
|
||||
|
||||
private async getNotebookContentsAsync(path: string): Promise<nb.INotebookContents> {
|
||||
if (!path) {
|
||||
return undefined;
|
||||
}
|
||||
// Note: intentionally letting caller handle exceptions
|
||||
let contentsModel = await this.contents.get(path);
|
||||
if (!contentsModel) {
|
||||
return undefined;
|
||||
}
|
||||
return <nb.INotebookContents>contentsModel.content;
|
||||
}
|
||||
|
||||
public async save(notebookUri: vscode.Uri, notebook: nb.INotebookContents): Promise<nb.INotebookContents> {
|
||||
let path = notebookUri.fsPath;
|
||||
await this.contents.save(path, {
|
||||
path: path,
|
||||
content: notebook,
|
||||
type: 'notebook',
|
||||
format: 'json'
|
||||
});
|
||||
return notebook;
|
||||
}
|
||||
}
|
||||
399
extensions/notebook/src/jupyter/serverInstance.ts
Normal file
399
extensions/notebook/src/jupyter/serverInstance.ts
Normal file
@@ -0,0 +1,399 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as os from 'os';
|
||||
import { spawn, ExecOptions, SpawnOptions, ChildProcess } from 'child_process';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import { IServerInstance } from './common';
|
||||
import JupyterServerInstallation from './jupyterServerInstallation';
|
||||
import * as utils from '../common/utils';
|
||||
import * as constants from '../common/constants';
|
||||
import * as notebookUtils from '../common/notebookUtils';
|
||||
import * as ports from '../common/ports';
|
||||
|
||||
const NotebookConfigFilename = 'jupyter_notebook_config.py';
|
||||
const CustomJsFilename = 'custom.js';
|
||||
const defaultPort = 8888;
|
||||
const JupyterStartedMessage = 'The Jupyter Notebook is running';
|
||||
|
||||
type MessageListener = (data: string | Buffer) => void;
|
||||
type ErrorListener = (err: any) => void;
|
||||
|
||||
export interface IInstanceOptions {
|
||||
/**
|
||||
* The path to the initial document we want to start this server for
|
||||
*/
|
||||
documentPath: string;
|
||||
|
||||
/**
|
||||
* Base install information needed in order to start the server instance
|
||||
*/
|
||||
install: JupyterServerInstallation;
|
||||
|
||||
/**
|
||||
* Optional start directory for the notebook server. If none is set, will use a
|
||||
* path relative to the initial document
|
||||
*/
|
||||
notebookDirectory?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to enable testing without calling into file system or
|
||||
* commandline shell APIs
|
||||
*/
|
||||
export class ServerInstanceUtils {
|
||||
public mkDir(dirPath: string, outputChannel?: vscode.OutputChannel): Promise<void> {
|
||||
return utils.mkDir(dirPath, outputChannel);
|
||||
}
|
||||
public removeDir(dirPath: string): Promise<void> {
|
||||
return fs.remove(dirPath);
|
||||
}
|
||||
public pathExists(dirPath: string): Promise<boolean> {
|
||||
return fs.pathExists(dirPath);
|
||||
}
|
||||
public copy(src: string, dest: string): Promise<void> {
|
||||
return fs.copy(src, dest);
|
||||
}
|
||||
public existsSync(dirPath: string): boolean {
|
||||
return fs.existsSync(dirPath);
|
||||
}
|
||||
public generateUuid(): string {
|
||||
return UUID.generateUuid();
|
||||
}
|
||||
public executeBufferedCommand(cmd: string, options: ExecOptions, outputChannel?: vscode.OutputChannel): Thenable<string> {
|
||||
return utils.executeBufferedCommand(cmd, options, outputChannel);
|
||||
}
|
||||
|
||||
public spawn(command: string, args?: ReadonlyArray<string>, options?: SpawnOptions): ChildProcess {
|
||||
return spawn(command, args, options);
|
||||
}
|
||||
|
||||
public checkProcessDied(childProcess: ChildProcess): void {
|
||||
if (!childProcess) {
|
||||
return;
|
||||
}
|
||||
// Wait 10 seconds and then force kill. Jupyter stop is slow so this seems a reasonable time limit
|
||||
setTimeout(() => {
|
||||
// Test if the process is still alive. Throws an exception if not
|
||||
try {
|
||||
process.kill(childProcess.pid, <any>0);
|
||||
} catch (error) {
|
||||
// All is fine.
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
export class PerNotebookServerInstance implements IServerInstance {
|
||||
|
||||
/**
|
||||
* Root of the jupyter directory structure. Config and data roots will be
|
||||
* under this, in order to simplify deletion of folders on stop of the instance
|
||||
*/
|
||||
private baseDir: string;
|
||||
|
||||
/**
|
||||
* Path to configuration folder for this instance. Typically:
|
||||
* %extension_path%/jupyter_config/%server%_config
|
||||
*/
|
||||
private instanceConfigRoot: string;
|
||||
|
||||
/**
|
||||
* Path to data folder for this instance. Typically:
|
||||
* %extension_path%/jupyter_config/%server%_data
|
||||
*/
|
||||
private instanceDataRoot: string;
|
||||
|
||||
private _systemJupyterDir: string;
|
||||
private _port: string;
|
||||
private _uri: vscode.Uri;
|
||||
private _isStarted: boolean = false;
|
||||
private utils: ServerInstanceUtils;
|
||||
private childProcess: ChildProcess;
|
||||
private errorHandler: ErrorHandler = new ErrorHandler();
|
||||
|
||||
constructor(private options: IInstanceOptions, fsUtils?: ServerInstanceUtils) {
|
||||
this.utils = fsUtils || new ServerInstanceUtils();
|
||||
}
|
||||
|
||||
public get isStarted(): boolean {
|
||||
return this._isStarted;
|
||||
}
|
||||
|
||||
public get port(): string {
|
||||
return this._port;
|
||||
}
|
||||
|
||||
public get uri(): vscode.Uri {
|
||||
return this._uri;
|
||||
}
|
||||
|
||||
public async configure(): Promise<void> {
|
||||
await this.configureJupyter();
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
await this.startInternal();
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
try {
|
||||
if (this.baseDir) {
|
||||
let exists = await this.utils.pathExists(this.baseDir);
|
||||
if (exists) {
|
||||
await this.utils.removeDir(this.baseDir);
|
||||
}
|
||||
}
|
||||
if (this.isStarted) {
|
||||
let install = this.options.install;
|
||||
let stopCommand = `${install.pythonExecutable} -m jupyter notebook stop ${this._port}`;
|
||||
await this.utils.executeBufferedCommand(stopCommand, install.execOptions, install.outputChannel);
|
||||
this._isStarted = false;
|
||||
this.utils.checkProcessDied(this.childProcess);
|
||||
this.handleConnectionClosed();
|
||||
}
|
||||
} catch (error) {
|
||||
// For now, we don't care as this is non-critical
|
||||
this.notify(this.options.install, localize('serverStopError', 'Error stopping Notebook Server: {0}', utils.getErrorMessage(error)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async configureJupyter(): Promise<void> {
|
||||
await this.createInstanceFolders();
|
||||
let resourcesFolder = path.join(this.options.install.extensionPath, 'resources', constants.jupyterConfigRootFolder);
|
||||
await this.copyInstanceConfig(resourcesFolder);
|
||||
await this.CopyCustomJs(resourcesFolder);
|
||||
await this.copyKernelsToSystemJupyterDirs();
|
||||
}
|
||||
|
||||
private async createInstanceFolders(): Promise<void> {
|
||||
this.baseDir = path.join(this.options.install.configRoot, 'instances', `${this.utils.generateUuid()}`);
|
||||
this.instanceConfigRoot = path.join(this.baseDir, 'config');
|
||||
this.instanceDataRoot = path.join(this.baseDir, 'data');
|
||||
await this.utils.mkDir(this.baseDir, this.options.install.outputChannel);
|
||||
await this.utils.mkDir(this.instanceConfigRoot, this.options.install.outputChannel);
|
||||
await this.utils.mkDir(this.instanceDataRoot, this.options.install.outputChannel);
|
||||
}
|
||||
|
||||
private async copyInstanceConfig(resourcesFolder: string): Promise<void> {
|
||||
let configSource = path.join(resourcesFolder, NotebookConfigFilename);
|
||||
let configDest = path.join(this.instanceConfigRoot, NotebookConfigFilename);
|
||||
await this.utils.copy(configSource, configDest);
|
||||
}
|
||||
|
||||
private async CopyCustomJs(resourcesFolder: string): Promise<void> {
|
||||
let customPath = path.join(this.instanceConfigRoot, 'custom');
|
||||
await this.utils.mkDir(customPath, this.options.install.outputChannel);
|
||||
let customSource = path.join(resourcesFolder, CustomJsFilename);
|
||||
let customDest = path.join(customPath, CustomJsFilename);
|
||||
await this.utils.copy(customSource, customDest);
|
||||
}
|
||||
|
||||
private async copyKernelsToSystemJupyterDirs(): Promise<void> {
|
||||
let kernelsExtensionSource = path.join(this.options.install.extensionPath, 'kernels');
|
||||
this._systemJupyterDir = this.getSystemJupyterKernelDir();
|
||||
if (!this.utils.existsSync(this._systemJupyterDir)) {
|
||||
await this.utils.mkDir(this._systemJupyterDir, this.options.install.outputChannel);
|
||||
}
|
||||
await this.utils.copy(kernelsExtensionSource, this._systemJupyterDir);
|
||||
}
|
||||
|
||||
private getSystemJupyterKernelDir(): string {
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
let appDataWindows = process.env['APPDATA'];
|
||||
return appDataWindows + '\\jupyter\\kernels';
|
||||
case 'darwin':
|
||||
return path.resolve(os.homedir(), 'Library/Jupyter/kernels');
|
||||
default:
|
||||
return path.resolve(os.homedir(), '.local/share/jupyter/kernels');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Starts a Jupyter instance using the provided a start command. Server is determined to have
|
||||
* started when the log message with URL to connect to is emitted.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
protected async startInternal(): Promise<void> {
|
||||
if (this.isStarted) {
|
||||
return;
|
||||
}
|
||||
let notebookDirectory = this.getNotebookDirectory();
|
||||
// Find a port in a given range. If run into trouble, got up 100 in range and search inside a larger range
|
||||
let port = await ports.strictFindFreePort(new ports.StrictPortFindOptions(defaultPort, defaultPort + 100, defaultPort + 1000));
|
||||
let token = await notebookUtils.getRandomToken();
|
||||
this._uri = vscode.Uri.parse(`http://localhost:${port}/?token=${token}`);
|
||||
this._port = port.toString();
|
||||
let startCommand = `${this.options.install.pythonExecutable} -m jupyter notebook --no-browser --notebook-dir "${notebookDirectory}" --port=${port} --NotebookApp.token=${token}`;
|
||||
this.notifyStarting(this.options.install, startCommand);
|
||||
|
||||
// Execute the command
|
||||
await this.executeStartCommand(startCommand);
|
||||
}
|
||||
|
||||
private executeStartCommand(startCommand: string): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let install = this.options.install;
|
||||
this.childProcess = this.spawnJupyterProcess(install, startCommand);
|
||||
|
||||
// Add listeners for the process exiting prematurely
|
||||
let onErrorBeforeStartup = (err) => reject(err);
|
||||
let onExitBeforeStart = (err) => {
|
||||
if (!this.isStarted) {
|
||||
reject(localize('notebookStartProcessExitPremature', 'Notebook process exited prematurely with error: {0}', err));
|
||||
}
|
||||
};
|
||||
this.childProcess.on('error', onErrorBeforeStartup);
|
||||
this.childProcess.on('exit', onExitBeforeStart);
|
||||
|
||||
// Add listener for the process to emit its web address
|
||||
let handleStdout = (data: string | Buffer) => { install.outputChannel.appendLine(data.toString()); };
|
||||
let handleStdErr = (data: string | Buffer) => {
|
||||
// For some reason, URL info is sent on StdErr
|
||||
let [url, port] = this.matchUrlAndPort(data);
|
||||
if (url) {
|
||||
// For now, will verify port matches
|
||||
if (url.authority !== this._uri.authority
|
||||
|| url.query !== this._uri.query) {
|
||||
this._uri = url;
|
||||
this._port = port;
|
||||
}
|
||||
this.notifyStarted(install, url.toString());
|
||||
this._isStarted = true;
|
||||
|
||||
this.updateListeners(handleStdout, handleStdErr, onErrorBeforeStartup, onExitBeforeStart);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
this.childProcess.stdout.on('data', handleStdout);
|
||||
this.childProcess.stderr.on('data', handleStdErr);
|
||||
});
|
||||
}
|
||||
|
||||
private updateListeners(handleStdout: MessageListener, handleStdErr: MessageListener, onErrorBeforeStartup: ErrorListener, onExitBeforeStart: ErrorListener): void {
|
||||
this.childProcess.stdout.removeListener('data', handleStdout);
|
||||
this.childProcess.stderr.removeListener('data', handleStdErr);
|
||||
this.childProcess.removeListener('error', onErrorBeforeStartup);
|
||||
this.childProcess.removeListener('exit', onExitBeforeStart);
|
||||
|
||||
this.childProcess.addListener('error', this.handleConnectionError);
|
||||
this.childProcess.addListener('exit', this.handleConnectionClosed);
|
||||
|
||||
// TODO #897 covers serializing stdout and stderr to a location where we can read from so that user can see if they run into trouble
|
||||
}
|
||||
|
||||
private handleConnectionError(error: Error): void {
|
||||
let action = this.errorHandler.handleError(error);
|
||||
if (action === ErrorAction.Shutdown) {
|
||||
this.notify(this.options.install, localize('jupyterError', 'Error sent from Jupyter: {0}', utils.getErrorMessage(error)));
|
||||
this.stop();
|
||||
}
|
||||
}
|
||||
private handleConnectionClosed(): void {
|
||||
this.childProcess = undefined;
|
||||
this._isStarted = false;
|
||||
}
|
||||
|
||||
getNotebookDirectory(): string {
|
||||
if (this.options.notebookDirectory) {
|
||||
if (this.options.notebookDirectory.endsWith('\\')) {
|
||||
return this.options.notebookDirectory.substr(0, this.options.notebookDirectory.length - 1) + '/';
|
||||
}
|
||||
return this.options.notebookDirectory;
|
||||
}
|
||||
return path.dirname(this.options.documentPath);
|
||||
}
|
||||
|
||||
private matchUrlAndPort(data: string | Buffer): [vscode.Uri, string] {
|
||||
// regex: Looks for the successful startup log message like:
|
||||
// [C 12:08:51.947 NotebookApp]
|
||||
//
|
||||
// Copy/paste this URL into your browser when you connect for the first time,
|
||||
// to login with a token:
|
||||
// http://localhost:8888/?token=f5ee846e9bd61c3a8d835ecd9b965591511a331417b997b7
|
||||
let dataString = data.toString();
|
||||
let urlMatch = dataString.match(/\[C[\s\S]+ {8}(.+:(\d+)\/.*)$/m);
|
||||
|
||||
if (urlMatch) {
|
||||
// Legacy case: manually parse token info if no token/port were passed
|
||||
return [vscode.Uri.parse(urlMatch[1]), urlMatch[2]];
|
||||
} else if (this._uri && dataString.indexOf(JupyterStartedMessage) > -1) {
|
||||
// Default case: detect the notebook started message, indicating our preferred port and token were used
|
||||
return [this._uri, this._port];
|
||||
}
|
||||
return [undefined, undefined];
|
||||
}
|
||||
|
||||
private notifyStarted(install: JupyterServerInstallation, jupyterUri: string): void {
|
||||
install.outputChannel.appendLine(localize('jupyterOutputMsgStartSuccessful', '... Jupyter is running at {0}', jupyterUri));
|
||||
}
|
||||
private notify(install: JupyterServerInstallation, message: string): void {
|
||||
install.outputChannel.appendLine(message);
|
||||
}
|
||||
|
||||
private notifyStarting(install: JupyterServerInstallation, startCommand: string): void {
|
||||
install.outputChannel.appendLine(localize('jupyterOutputMsgStart', '... Starting Notebook server'));
|
||||
install.outputChannel.appendLine(` > ${startCommand}`);
|
||||
}
|
||||
|
||||
private spawnJupyterProcess(install: JupyterServerInstallation, startCommand: string): ChildProcess {
|
||||
// Specify the global environment variables
|
||||
let env = this.getEnvWithConfigPaths();
|
||||
// Setting the PATH variable here for the jupyter command. Apparently setting it above will cause the
|
||||
// notebook process to die even though we don't override it with the for loop logic above.
|
||||
delete env['Path'];
|
||||
env['PATH'] = install.pythonEnvVarPath;
|
||||
|
||||
// 'MSHOST_TELEMETRY_ENABLED' and 'MSHOST_ENVIRONMENT' environment variables are set
|
||||
// for telemetry purposes used by PROSE in the process where the Jupyter kernel runs
|
||||
if (vscode.workspace.getConfiguration('telemetry').get<boolean>('enableTelemetry', true)) {
|
||||
env['MSHOST_TELEMETRY_ENABLED'] = true;
|
||||
} else {
|
||||
env['MSHOST_TELEMETRY_ENABLED'] = false;
|
||||
}
|
||||
|
||||
env['MSHOST_ENVIRONMENT'] = 'ADSClient-' + vscode.version;
|
||||
|
||||
// Start the notebook process
|
||||
let options = {
|
||||
shell: true,
|
||||
env: env
|
||||
};
|
||||
let childProcess = this.utils.spawn(startCommand, [], options);
|
||||
return childProcess;
|
||||
}
|
||||
|
||||
private getEnvWithConfigPaths(): any {
|
||||
let env = Object.assign({}, process.env);
|
||||
env['JUPYTER_CONFIG_DIR'] = this.instanceConfigRoot;
|
||||
env['JUPYTER_PATH'] = this.instanceDataRoot;
|
||||
return env;
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorHandler {
|
||||
private numErrors: number = 0;
|
||||
|
||||
public handleError(error: Error): ErrorAction {
|
||||
this.numErrors++;
|
||||
return this.numErrors > 3 ? ErrorAction.Shutdown : ErrorAction.Continue;
|
||||
}
|
||||
}
|
||||
|
||||
enum ErrorAction {
|
||||
Continue = 1,
|
||||
Shutdown = 2
|
||||
}
|
||||
173
extensions/notebook/src/prompts/adapter.ts
Normal file
173
extensions/notebook/src/prompts/adapter.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
'use strict';
|
||||
|
||||
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
|
||||
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
|
||||
|
||||
import { window, OutputChannel } from 'vscode';
|
||||
import * as Constants from '../common/constants';
|
||||
import * as nodeUtil from 'util';
|
||||
import PromptFactory from './factory';
|
||||
import EscapeException from './escapeException';
|
||||
import { IQuestion, IPrompter, IPromptCallback } from './question';
|
||||
|
||||
// Supports simple pattern for prompting for user input and acting on this
|
||||
export default class CodeAdapter implements IPrompter {
|
||||
|
||||
private outChannel: OutputChannel;
|
||||
private outBuffer: string = '';
|
||||
private messageLevelFormatters = {};
|
||||
constructor() {
|
||||
// TODO Decide whether output channel logging should be saved here?
|
||||
this.outChannel = window.createOutputChannel(Constants.outputChannelName);
|
||||
// this.outChannel.clear();
|
||||
}
|
||||
|
||||
public logError(message: any): void {
|
||||
let line = `error: ${message.message}\n Code - ${message.code}`;
|
||||
|
||||
this.outBuffer += `${line}\n`;
|
||||
this.outChannel.appendLine(line);
|
||||
}
|
||||
|
||||
// private formatInfo(message: any) {
|
||||
// const prefix = `${message.level}: (${message.id}) `;
|
||||
// if (message.id === "json") {
|
||||
// let jsonString = JSON.stringify(message.data, undefined, 4);
|
||||
// return `${prefix}${message.message}\n${jsonString}`;
|
||||
// }
|
||||
// else {
|
||||
// return `${prefix}${message.message}`;
|
||||
// }
|
||||
// }
|
||||
|
||||
// private formatAction(message: any) {
|
||||
// const prefix = `info: ${message.level}: (${message.id}) `;
|
||||
// return `${prefix}${message.message}`;
|
||||
// }
|
||||
|
||||
private formatMessage(message: any): string {
|
||||
const prefix = `${message.level}: (${message.id}) `;
|
||||
return `${prefix}${message.message}`;
|
||||
}
|
||||
|
||||
// private formatConflict(message: any) {
|
||||
// var msg = message.message + ':\n';
|
||||
// var picks = (<any[]>message.data.picks);
|
||||
// var pickCount = 1;
|
||||
// picks.forEach((pick) => {
|
||||
// let pickMessage = (pickCount++).toString() + "). " + pick.endpoint.name + "#" + pick.endpoint.target;
|
||||
// if (pick.pkgMeta._resolution && pick.pkgMeta._resolution.tag) {
|
||||
// pickMessage += " which resolved to " + pick.pkgMeta._resolution.tag
|
||||
// }
|
||||
// if (Array.isArray(pick.dependants) && pick.dependants.length > 0) {
|
||||
// pickMessage += " and is required by ";
|
||||
// pick.dependants.forEach((dep) => {
|
||||
// pickMessage += " " + dep.endpoint.name + "#" + dep.endpoint.target;
|
||||
// });
|
||||
// }
|
||||
// msg += " " + pickMessage + "\n";
|
||||
// });
|
||||
|
||||
// var prefix = (message.id === "solved"? "info" : "warn") + `: ${message.level}: (${message.id}) `;
|
||||
// return prefix + msg;
|
||||
// }
|
||||
|
||||
public log(message: any): void {
|
||||
let line: string = '';
|
||||
if (message && typeof (message.level) === 'string') {
|
||||
let formatter: (a: any) => string = this.formatMessage;
|
||||
if (this.messageLevelFormatters[message.level]) {
|
||||
formatter = this.messageLevelFormatters[message.level];
|
||||
}
|
||||
line = formatter(message);
|
||||
} else {
|
||||
line = nodeUtil.format(arguments);
|
||||
}
|
||||
|
||||
this.outBuffer += `${line}\n`;
|
||||
this.outChannel.appendLine(line);
|
||||
}
|
||||
|
||||
public clearLog(): void {
|
||||
this.outChannel.clear();
|
||||
}
|
||||
|
||||
public showLog(): void {
|
||||
this.outChannel.show();
|
||||
}
|
||||
|
||||
// TODO define question interface
|
||||
private fixQuestion(question: any): any {
|
||||
if (question.type === 'checkbox' && Array.isArray(question.choices)) {
|
||||
// For some reason when there's a choice of checkboxes, they aren't formatted properly
|
||||
// Not sure where the issue is
|
||||
question.choices = question.choices.map(item => {
|
||||
if (typeof (item) === 'string') {
|
||||
return { checked: false, name: item, value: item };
|
||||
} else {
|
||||
return item;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public promptSingle<T>(question: IQuestion, ignoreFocusOut?: boolean): Promise<T> {
|
||||
let questions: IQuestion[] = [question];
|
||||
return this.prompt(questions, ignoreFocusOut).then((answers: { [key: string]: T }) => {
|
||||
if (answers) {
|
||||
let response: T = answers[question.name];
|
||||
return response || undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public prompt<T>(questions: IQuestion[], ignoreFocusOut?: boolean): Promise<{ [key: string]: T }> {
|
||||
let answers: { [key: string]: T } = {};
|
||||
|
||||
// Collapse multiple questions into a set of prompt steps
|
||||
let promptResult: Promise<{ [key: string]: T }> = questions.reduce((promise: Promise<{ [key: string]: T }>, question: IQuestion) => {
|
||||
this.fixQuestion(question);
|
||||
|
||||
return promise.then(() => {
|
||||
return PromptFactory.createPrompt(question, ignoreFocusOut);
|
||||
}).then(prompt => {
|
||||
// Original Code: uses jQuery patterns. Keeping for reference
|
||||
// if (!question.when || question.when(answers) === true) {
|
||||
// return prompt.render().then(result => {
|
||||
// answers[question.name] = question.filter ? question.filter(result) : result;
|
||||
// });
|
||||
// }
|
||||
|
||||
if (!question.shouldPrompt || question.shouldPrompt(answers) === true) {
|
||||
return prompt.render().then(result => {
|
||||
answers[question.name] = result;
|
||||
|
||||
if (question.onAnswered) {
|
||||
question.onAnswered(result);
|
||||
}
|
||||
return answers;
|
||||
});
|
||||
}
|
||||
return answers;
|
||||
});
|
||||
}, Promise.resolve());
|
||||
|
||||
return promptResult.catch(err => {
|
||||
if (err instanceof EscapeException || err instanceof TypeError) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
window.showErrorMessage(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to make it possible to prompt using callback pattern. Generally Promise is a preferred flow
|
||||
public promptCallback(questions: IQuestion[], callback: IPromptCallback): void {
|
||||
// Collapse multiple questions into a set of prompt steps
|
||||
this.prompt(questions).then(answers => {
|
||||
if (callback) {
|
||||
callback(answers);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
52
extensions/notebook/src/prompts/checkbox.ts
Normal file
52
extensions/notebook/src/prompts/checkbox.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
|
||||
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
|
||||
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
|
||||
|
||||
import { window } from 'vscode';
|
||||
import Prompt from './prompt';
|
||||
import EscapeException from './escapeException';
|
||||
|
||||
const figures = require('figures');
|
||||
|
||||
export default class CheckboxPrompt extends Prompt {
|
||||
|
||||
constructor(question: any, ignoreFocusOut?: boolean) {
|
||||
super(question, ignoreFocusOut);
|
||||
}
|
||||
|
||||
public render(): any {
|
||||
let choices = this._question.choices.reduce((result, choice) => {
|
||||
let choiceName = choice.name || choice;
|
||||
result[`${choice.checked === true ? figures.radioOn : figures.radioOff} ${choiceName}`] = choice;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
let options = this.defaultQuickPickOptions;
|
||||
options.placeHolder = this._question.message;
|
||||
|
||||
let quickPickOptions = Object.keys(choices);
|
||||
quickPickOptions.push(figures.tick);
|
||||
|
||||
return window.showQuickPick(quickPickOptions, options)
|
||||
.then(result => {
|
||||
if (result === undefined) {
|
||||
throw new EscapeException();
|
||||
}
|
||||
|
||||
if (result !== figures.tick) {
|
||||
choices[result].checked = !choices[result].checked;
|
||||
|
||||
return this.render();
|
||||
}
|
||||
|
||||
return this._question.choices.reduce((result2, choice) => {
|
||||
if (choice.checked === true) {
|
||||
result2.push(choice.value);
|
||||
}
|
||||
|
||||
return result2;
|
||||
}, []);
|
||||
});
|
||||
}
|
||||
}
|
||||
34
extensions/notebook/src/prompts/confirm.ts
Normal file
34
extensions/notebook/src/prompts/confirm.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
|
||||
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
|
||||
|
||||
import { window } from 'vscode';
|
||||
import Prompt from './prompt';
|
||||
import LocalizedConstants = require('../common/localizedConstants');
|
||||
import EscapeException from './escapeException';
|
||||
|
||||
export default class ConfirmPrompt extends Prompt {
|
||||
|
||||
constructor(question: any, ignoreFocusOut?: boolean) {
|
||||
super(question, ignoreFocusOut);
|
||||
}
|
||||
|
||||
public render(): any {
|
||||
let choices: { [id: string]: boolean } = {};
|
||||
choices[LocalizedConstants.msgYes] = true;
|
||||
choices[LocalizedConstants.msgNo] = false;
|
||||
|
||||
let options = this.defaultQuickPickOptions;
|
||||
options.placeHolder = this._question.message;
|
||||
|
||||
return window.showQuickPick(Object.keys(choices), options)
|
||||
.then(result => {
|
||||
if (result === undefined) {
|
||||
throw new EscapeException();
|
||||
}
|
||||
|
||||
return choices[result] || false;
|
||||
});
|
||||
}
|
||||
}
|
||||
3
extensions/notebook/src/prompts/escapeException.ts
Normal file
3
extensions/notebook/src/prompts/escapeException.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
export default require('error-ex')('EscapeException');
|
||||
78
extensions/notebook/src/prompts/expand.ts
Normal file
78
extensions/notebook/src/prompts/expand.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
'use strict';
|
||||
|
||||
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
|
||||
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
|
||||
|
||||
import vscode = require('vscode');
|
||||
import Prompt from './prompt';
|
||||
import EscapeException from './escapeException';
|
||||
import { INameValueChoice } from './question';
|
||||
|
||||
const figures = require('figures');
|
||||
|
||||
export default class ExpandPrompt extends Prompt {
|
||||
|
||||
constructor(question: any, ignoreFocusOut?: boolean) {
|
||||
super(question, ignoreFocusOut);
|
||||
}
|
||||
|
||||
public render(): any {
|
||||
// label indicates this is a quickpick item. Otherwise it's a name-value pair
|
||||
if (this._question.choices[0].label) {
|
||||
return this.renderQuickPick(this._question.choices);
|
||||
} else {
|
||||
return this.renderNameValueChoice(this._question.choices);
|
||||
}
|
||||
}
|
||||
|
||||
private renderQuickPick(choices: vscode.QuickPickItem[]): any {
|
||||
let options = this.defaultQuickPickOptions;
|
||||
options.placeHolder = this._question.message;
|
||||
|
||||
return vscode.window.showQuickPick(choices, options)
|
||||
.then(result => {
|
||||
if (result === undefined) {
|
||||
throw new EscapeException();
|
||||
}
|
||||
|
||||
return this.validateAndReturn(result || false);
|
||||
});
|
||||
}
|
||||
private renderNameValueChoice(choices: INameValueChoice[]): any {
|
||||
const choiceMap = this._question.choices.reduce((result, choice) => {
|
||||
result[choice.name] = choice.value;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
let options = this.defaultQuickPickOptions;
|
||||
options.placeHolder = this._question.message;
|
||||
|
||||
return vscode.window.showQuickPick(Object.keys(choiceMap), options)
|
||||
.then(result => {
|
||||
if (result === undefined) {
|
||||
throw new EscapeException();
|
||||
}
|
||||
|
||||
// Note: cannot be used with 0 or false responses
|
||||
let returnVal = choiceMap[result] || false;
|
||||
return this.validateAndReturn(returnVal);
|
||||
});
|
||||
}
|
||||
|
||||
private validateAndReturn(value: any): any {
|
||||
if (!this.validate(value)) {
|
||||
return this.render();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private validate(value: any): boolean {
|
||||
const validationError = this._question.validate ? this._question.validate(value || '') : undefined;
|
||||
|
||||
if (validationError) {
|
||||
this._question.message = `${figures.warning} ${validationError}`;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
39
extensions/notebook/src/prompts/factory.ts
Normal file
39
extensions/notebook/src/prompts/factory.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
'use strict';
|
||||
|
||||
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
|
||||
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
|
||||
|
||||
import Prompt from './prompt';
|
||||
import InputPrompt from './input';
|
||||
import PasswordPrompt from './password';
|
||||
import ListPrompt from './list';
|
||||
import ConfirmPrompt from './confirm';
|
||||
import CheckboxPrompt from './checkbox';
|
||||
import ExpandPrompt from './expand';
|
||||
|
||||
export default class PromptFactory {
|
||||
|
||||
public static createPrompt(question: any, ignoreFocusOut?: boolean): Prompt {
|
||||
/**
|
||||
* TODO:
|
||||
* - folder
|
||||
*/
|
||||
switch (question.type || 'input') {
|
||||
case 'string':
|
||||
case 'input':
|
||||
return new InputPrompt(question, ignoreFocusOut);
|
||||
case 'password':
|
||||
return new PasswordPrompt(question, ignoreFocusOut);
|
||||
case 'list':
|
||||
return new ListPrompt(question, ignoreFocusOut);
|
||||
case 'confirm':
|
||||
return new ConfirmPrompt(question, ignoreFocusOut);
|
||||
case 'checkbox':
|
||||
return new CheckboxPrompt(question, ignoreFocusOut);
|
||||
case 'expand':
|
||||
return new ExpandPrompt(question, ignoreFocusOut);
|
||||
default:
|
||||
throw new Error(`Could not find a prompt for question type ${question.type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
59
extensions/notebook/src/prompts/input.ts
Normal file
59
extensions/notebook/src/prompts/input.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
'use strict';
|
||||
|
||||
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
|
||||
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
|
||||
|
||||
import { window, InputBoxOptions } from 'vscode';
|
||||
import Prompt from './prompt';
|
||||
import EscapeException from './escapeException';
|
||||
|
||||
const figures = require('figures');
|
||||
|
||||
export default class InputPrompt extends Prompt {
|
||||
|
||||
protected _options: InputBoxOptions;
|
||||
|
||||
constructor(question: any, ignoreFocusOut?: boolean) {
|
||||
super(question, ignoreFocusOut);
|
||||
|
||||
this._options = this.defaultInputBoxOptions;
|
||||
this._options.prompt = this._question.message;
|
||||
}
|
||||
|
||||
// Helper for callers to know the right type to get from the type factory
|
||||
public static get promptType(): string { return 'input'; }
|
||||
|
||||
public render(): any {
|
||||
// Prefer default over the placeHolder, if specified
|
||||
let placeHolder = this._question.default ? this._question.default : this._question.placeHolder;
|
||||
|
||||
if (this._question.default instanceof Error) {
|
||||
placeHolder = this._question.default.message;
|
||||
this._question.default = undefined;
|
||||
}
|
||||
|
||||
this._options.placeHolder = placeHolder;
|
||||
|
||||
return window.showInputBox(this._options)
|
||||
.then(result => {
|
||||
if (result === undefined) {
|
||||
throw new EscapeException();
|
||||
}
|
||||
|
||||
if (result === '') {
|
||||
// Use the default value, if defined
|
||||
result = this._question.default || '';
|
||||
}
|
||||
|
||||
const validationError = this._question.validate ? this._question.validate(result || '') : undefined;
|
||||
|
||||
if (validationError) {
|
||||
this._question.default = new Error(`${figures.warning} ${validationError}`);
|
||||
|
||||
return this.render();
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
||||
34
extensions/notebook/src/prompts/list.ts
Normal file
34
extensions/notebook/src/prompts/list.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
|
||||
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
|
||||
|
||||
import { window } from 'vscode';
|
||||
import Prompt from './prompt';
|
||||
import EscapeException from './escapeException';
|
||||
|
||||
export default class ListPrompt extends Prompt {
|
||||
|
||||
constructor(question: any, ignoreFocusOut?: boolean) {
|
||||
super(question, ignoreFocusOut);
|
||||
}
|
||||
|
||||
public render(): any {
|
||||
const choices = this._question.choices.reduce((result, choice) => {
|
||||
result[choice.name] = choice.value;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
let options = this.defaultQuickPickOptions;
|
||||
options.placeHolder = this._question.message;
|
||||
|
||||
return window.showQuickPick(Object.keys(choices), options)
|
||||
.then(result => {
|
||||
if (result === undefined) {
|
||||
throw new EscapeException();
|
||||
}
|
||||
|
||||
return choices[result];
|
||||
});
|
||||
}
|
||||
}
|
||||
14
extensions/notebook/src/prompts/password.ts
Normal file
14
extensions/notebook/src/prompts/password.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
|
||||
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
|
||||
import InputPrompt from './input';
|
||||
|
||||
export default class PasswordPrompt extends InputPrompt {
|
||||
|
||||
constructor(question: any, ignoreFocusOut?: boolean) {
|
||||
super(question, ignoreFocusOut);
|
||||
|
||||
this._options.password = true;
|
||||
}
|
||||
}
|
||||
70
extensions/notebook/src/prompts/progressIndicator.ts
Normal file
70
extensions/notebook/src/prompts/progressIndicator.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
'use strict';
|
||||
|
||||
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
|
||||
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
|
||||
|
||||
import { window, StatusBarItem, StatusBarAlignment } from 'vscode';
|
||||
|
||||
export default class ProgressIndicator {
|
||||
|
||||
private _statusBarItem: StatusBarItem;
|
||||
|
||||
constructor() {
|
||||
this._statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left);
|
||||
}
|
||||
|
||||
private _tasks: string[] = [];
|
||||
public beginTask(task: string): void {
|
||||
this._tasks.push(task);
|
||||
this.displayProgressIndicator();
|
||||
}
|
||||
|
||||
public endTask(task: string): void {
|
||||
if (this._tasks.length > 0) {
|
||||
this._tasks.pop();
|
||||
}
|
||||
|
||||
this.setMessage();
|
||||
}
|
||||
|
||||
private setMessage(): void {
|
||||
if (this._tasks.length === 0) {
|
||||
this._statusBarItem.text = '';
|
||||
this.hideProgressIndicator();
|
||||
return;
|
||||
}
|
||||
|
||||
this._statusBarItem.text = this._tasks[this._tasks.length - 1];
|
||||
this._statusBarItem.show();
|
||||
}
|
||||
|
||||
private _interval: any;
|
||||
private displayProgressIndicator(): void {
|
||||
this.setMessage();
|
||||
this.hideProgressIndicator();
|
||||
this._interval = setInterval(() => this.onDisplayProgressIndicator(), 100);
|
||||
}
|
||||
private hideProgressIndicator(): void {
|
||||
if (this._interval) {
|
||||
clearInterval(this._interval);
|
||||
this._interval = undefined;
|
||||
}
|
||||
this.ProgressCounter = 0;
|
||||
}
|
||||
|
||||
private ProgressText = ['|', '/', '-', '\\', '|', '/', '-', '\\'];
|
||||
private ProgressCounter = 0;
|
||||
private onDisplayProgressIndicator(): void {
|
||||
if (this._tasks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let txt = this.ProgressText[this.ProgressCounter];
|
||||
this._statusBarItem.text = this._tasks[this._tasks.length - 1] + ' ' + txt;
|
||||
this.ProgressCounter++;
|
||||
|
||||
if (this.ProgressCounter >= this.ProgressText.length - 1) {
|
||||
this.ProgressCounter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
extensions/notebook/src/prompts/prompt.ts
Normal file
33
extensions/notebook/src/prompts/prompt.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
|
||||
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
|
||||
|
||||
import { InputBoxOptions, QuickPickOptions } from 'vscode';
|
||||
|
||||
abstract class Prompt {
|
||||
|
||||
protected _question: any;
|
||||
protected _ignoreFocusOut?: boolean;
|
||||
|
||||
constructor(question: any, ignoreFocusOut?: boolean) {
|
||||
this._question = question;
|
||||
this._ignoreFocusOut = ignoreFocusOut ? ignoreFocusOut : false;
|
||||
}
|
||||
|
||||
public abstract render(): any;
|
||||
|
||||
protected get defaultQuickPickOptions(): QuickPickOptions {
|
||||
return {
|
||||
ignoreFocusOut: this._ignoreFocusOut
|
||||
};
|
||||
}
|
||||
|
||||
protected get defaultInputBoxOptions(): InputBoxOptions {
|
||||
return {
|
||||
ignoreFocusOut: this._ignoreFocusOut
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default Prompt;
|
||||
68
extensions/notebook/src/prompts/question.ts
Normal file
68
extensions/notebook/src/prompts/question.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import vscode = require('vscode');
|
||||
|
||||
export class QuestionTypes {
|
||||
public static get input(): string { return 'input'; }
|
||||
public static get password(): string { return 'password'; }
|
||||
public static get list(): string { return 'list'; }
|
||||
public static get confirm(): string { return 'confirm'; }
|
||||
public static get checkbox(): string { return 'checkbox'; }
|
||||
public static get expand(): string { return 'expand'; }
|
||||
}
|
||||
|
||||
// Question interface to clarify how to use the prompt feature
|
||||
// based on Bower Question format: https://github.com/bower/bower/blob/89069784bb46bfd6639b4a75e98a0d7399a8c2cb/packages/bower-logger/README.md
|
||||
export interface IQuestion {
|
||||
// Type of question (see QuestionTypes)
|
||||
type: string;
|
||||
// Name of the question for disambiguation
|
||||
name: string;
|
||||
// Message to display to the user
|
||||
message: string;
|
||||
// Optional placeHolder to give more detailed information to the user
|
||||
placeHolder?: any;
|
||||
// Optional default value - this will be used instead of placeHolder
|
||||
default?: any;
|
||||
// optional set of choices to be used. Can be QuickPickItems or a simple name-value pair
|
||||
choices?: Array<vscode.QuickPickItem | INameValueChoice>;
|
||||
// Optional validation function that returns an error string if validation fails
|
||||
validate?: (value: any) => string;
|
||||
// Optional pre-prompt function. Takes in set of answers so far, and returns true if prompt should occur
|
||||
shouldPrompt?: (answers: { [id: string]: any }) => boolean;
|
||||
// Optional action to take on the question being answered
|
||||
onAnswered?: (value: any) => void;
|
||||
// Optional set of options to support matching choices.
|
||||
matchOptions?: vscode.QuickPickOptions;
|
||||
}
|
||||
|
||||
// Pair used to display simple choices to the user
|
||||
export interface INameValueChoice {
|
||||
name: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
// Generic object that can be used to define a set of questions and handle the result
|
||||
export interface IQuestionHandler {
|
||||
// Set of questions to be answered
|
||||
questions: IQuestion[];
|
||||
// Optional callback, since questions may handle themselves
|
||||
callback?: IPromptCallback;
|
||||
}
|
||||
|
||||
export interface IPrompter {
|
||||
promptSingle<T>(question: IQuestion, ignoreFocusOut?: boolean): Promise<T>;
|
||||
/**
|
||||
* Prompts for multiple questions
|
||||
*
|
||||
* @returns {[questionId: string]: T} Map of question IDs to results, or undefined if
|
||||
* the user canceled the question session
|
||||
*/
|
||||
prompt<T>(questions: IQuestion[], ignoreFocusOut?: boolean): Promise<{ [questionId: string]: any }>;
|
||||
promptCallback(questions: IQuestion[], callback: IPromptCallback): void;
|
||||
}
|
||||
|
||||
export interface IPromptCallback {
|
||||
(answers: { [id: string]: any }): void;
|
||||
}
|
||||
259
extensions/notebook/src/test/common.ts
Normal file
259
extensions/notebook/src/test/common.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { IServerInstance } from '../jupyter/common';
|
||||
import { Session, Kernel, KernelMessage, ServerConnection } from '@jupyterlab/services';
|
||||
import { ISignal } from '@phosphor/signaling';
|
||||
|
||||
export class JupyterServerInstanceStub implements IServerInstance {
|
||||
public get port(): string {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public get uri(): vscode.Uri {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public configure(): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public start(): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
stop(): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//#region sesion and kernel stubs (long)
|
||||
export class SessionStub implements Session.ISession {
|
||||
public get terminated(): ISignal<this, void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public get kernelChanged(): ISignal<this, Session.IKernelChangedArgs> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public get statusChanged(): ISignal<this, Kernel.Status> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public get propertyChanged(): ISignal<this, 'path' | 'name' | 'type'> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public get iopubMessage(): ISignal<this, KernelMessage.IIOPubMessage> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public get unhandledMessage(): ISignal<this, KernelMessage.IMessage> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public get anyMessage(): ISignal<this, Kernel.IAnyMessageArgs> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public get id(): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public get path(): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public get name(): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public get type(): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public get serverSettings(): ServerConnection.ISettings {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public get model(): Session.IModel {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public get kernel(): Kernel.IKernelConnection {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public get status(): Kernel.Status {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public get isDisposed(): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
setPath(path: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
setName(name: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
setType(type: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
changeKernel(options: Partial<Kernel.IModel>): Promise<Kernel.IKernelConnection> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
shutdown(): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
dispose(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export class KernelStub implements Kernel.IKernel {
|
||||
get terminated(): ISignal<this, void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
get statusChanged(): ISignal<this, Kernel.Status> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
get iopubMessage(): ISignal<this, KernelMessage.IIOPubMessage> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
get unhandledMessage(): ISignal<this, KernelMessage.IMessage> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
get anyMessage(): ISignal<this, Kernel.IAnyMessageArgs> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
get serverSettings(): ServerConnection.ISettings {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
get id(): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
get name(): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
get model(): Kernel.IModel {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
get username(): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
get clientId(): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
get status(): Kernel.Status {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
get info(): KernelMessage.IInfoReply {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
get isReady(): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
get ready(): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
get isDisposed(): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
shutdown(): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getSpec(): Promise<Kernel.ISpecModel> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
sendShellMessage(msg: KernelMessage.IShellMessage, expectReply?: boolean, disposeOnDone?: boolean): Kernel.IFuture {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
reconnect(): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
interrupt(): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
restart(): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
requestKernelInfo(): Promise<KernelMessage.IInfoReplyMsg> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
requestComplete(content: KernelMessage.ICompleteRequest): Promise<KernelMessage.ICompleteReplyMsg> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
requestInspect(content: KernelMessage.IInspectRequest): Promise<KernelMessage.IInspectReplyMsg> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
requestHistory(content: KernelMessage.IHistoryRequest): Promise<KernelMessage.IHistoryReplyMsg> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
requestExecute(content: KernelMessage.IExecuteRequest, disposeOnDone?: boolean): Kernel.IFuture {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
requestIsComplete(content: KernelMessage.IIsCompleteRequest): Promise<KernelMessage.IIsCompleteReplyMsg> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
requestCommInfo(content: KernelMessage.ICommInfoRequest): Promise<KernelMessage.ICommInfoReplyMsg> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
sendInputReply(content: KernelMessage.IInputReply): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
connectToComm(targetName: string, commId?: string): Kernel.IComm {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
registerCommTarget(targetName: string, callback: (comm: Kernel.IComm, msg: KernelMessage.ICommOpenMsg) => void | PromiseLike<void>): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
removeCommTarget(targetName: string, callback: (comm: Kernel.IComm, msg: KernelMessage.ICommOpenMsg) => void | PromiseLike<void>): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
registerMessageHook(msgId: string, hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
removeMessageHook(msgId: string, hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
dispose(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export class FutureStub implements Kernel.IFuture {
|
||||
get msg(): KernelMessage.IShellMessage {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
get done(): Promise<KernelMessage.IShellMessage> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
get isDisposed(): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
get onReply(): (msg: KernelMessage.IShellMessage) => void | PromiseLike<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
set onReply(handler: (msg: KernelMessage.IShellMessage) => void | PromiseLike<void>) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
get onStdin(): (msg: KernelMessage.IStdinMessage) => void | PromiseLike<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
set onStdin(handler: (msg: KernelMessage.IStdinMessage) => void | PromiseLike<void>) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
get onIOPub(): (msg: KernelMessage.IIOPubMessage) => void | PromiseLike<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
set onIOPub(handler: (msg: KernelMessage.IIOPubMessage) => void | PromiseLike<void>) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
registerMessageHook(hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
removeMessageHook(hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
sendInputReply(content: KernelMessage.IInputReply): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
dispose(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
66
extensions/notebook/src/test/common/port.test.ts
Normal file
66
extensions/notebook/src/test/common/port.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// This code is originally from https://github.com/Microsoft/vscode/blob/master/src/vs/base/test/node/port.test.ts
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as net from 'net';
|
||||
import 'mocha';
|
||||
|
||||
import * as ports from '../../common/ports';
|
||||
|
||||
describe('Ports', () => {
|
||||
it('Should Find a free port (no timeout)', function (done): void {
|
||||
this.timeout(1000 * 10); // higher timeout for this test
|
||||
|
||||
// get an initial freeport >= 7000
|
||||
ports.findFreePort(7000, 100, 300000).then(initialPort => {
|
||||
assert.ok(initialPort >= 7000);
|
||||
|
||||
// create a server to block this port
|
||||
const server = net.createServer();
|
||||
server.listen(initialPort, undefined, undefined, () => {
|
||||
|
||||
// once listening, find another free port and assert that the port is different from the opened one
|
||||
ports.findFreePort(7000, 50, 300000).then(freePort => {
|
||||
assert.ok(freePort >= 7000 && freePort !== initialPort);
|
||||
server.close();
|
||||
|
||||
done();
|
||||
}, err => done(err));
|
||||
});
|
||||
}, err => done(err));
|
||||
});
|
||||
|
||||
it('Should Find a free port in strict mode', function (done): void {
|
||||
this.timeout(1000 * 10); // higher timeout for this test
|
||||
|
||||
// get an initial freeport >= 7000
|
||||
let options = new ports.StrictPortFindOptions(7000, 7100, 7200);
|
||||
options.timeout = 300000;
|
||||
ports.strictFindFreePort(options).then(initialPort => {
|
||||
assert.ok(initialPort >= 7000);
|
||||
|
||||
// create a server to block this port
|
||||
const server = net.createServer();
|
||||
server.listen(initialPort, undefined, undefined, () => {
|
||||
|
||||
// once listening, find another free port and assert that the port is different from the opened one
|
||||
options.startPort = initialPort;
|
||||
options.maxRetriesPerStartPort = 1;
|
||||
options.totalRetryLoops = 50;
|
||||
ports.strictFindFreePort(options).then(freePort => {
|
||||
assert.ok(freePort >= 7100 && freePort !== initialPort);
|
||||
server.close();
|
||||
|
||||
done();
|
||||
}, err => done(err));
|
||||
});
|
||||
}, err => done(err));
|
||||
});
|
||||
});
|
||||
|
||||
24
extensions/notebook/src/test/common/querybookUtils.test.ts
Normal file
24
extensions/notebook/src/test/common/querybookUtils.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as should from 'should';
|
||||
import 'mocha';
|
||||
|
||||
import * as notebookUtils from '../../common/notebookUtils';
|
||||
|
||||
describe('Random Token', () => {
|
||||
it('Should have default length and be hex only', async function (): Promise<void> {
|
||||
|
||||
let token = await notebookUtils.getRandomToken();
|
||||
should(token).have.length(48);
|
||||
let validChars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
|
||||
for (let i = 0; i < token.length; i++) {
|
||||
let char = token.charAt(i);
|
||||
should(validChars.indexOf(char)).be.greaterThan(-1);
|
||||
}
|
||||
});
|
||||
});
|
||||
46
extensions/notebook/src/test/common/stubs.ts
Normal file
46
extensions/notebook/src/test/common/stubs.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class MockExtensionContext implements vscode.ExtensionContext {
|
||||
logger: undefined;
|
||||
logDirectory: './';
|
||||
subscriptions: { dispose(): any; }[];
|
||||
workspaceState: vscode.Memento;
|
||||
globalState: vscode.Memento;
|
||||
extensionPath: string;
|
||||
asAbsolutePath(relativePath: string): string {
|
||||
return relativePath;
|
||||
}
|
||||
storagePath: string;
|
||||
|
||||
constructor() {
|
||||
this.subscriptions = [];
|
||||
}
|
||||
}
|
||||
|
||||
export class MockOutputChannel implements vscode.OutputChannel {
|
||||
name: string;
|
||||
|
||||
append(value: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
appendLine(value: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
clear(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
show(preserveFocus?: boolean): void;
|
||||
show(column?: vscode.ViewColumn, preserveFocus?: boolean): void;
|
||||
show(column?: any, preserveFocus?: any): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
hide(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
dispose(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
22
extensions/notebook/src/test/common/testUtils.ts
Normal file
22
extensions/notebook/src/test/common/testUtils.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
|
||||
export async function assertThrowsAsync(fn, regExp): Promise<void> {
|
||||
let f = () => {
|
||||
// Empty
|
||||
};
|
||||
try {
|
||||
await fn();
|
||||
} catch (e) {
|
||||
f = () => { throw e; };
|
||||
} finally {
|
||||
assert.throws(f, regExp);
|
||||
}
|
||||
}
|
||||
32
extensions/notebook/src/test/index.ts
Normal file
32
extensions/notebook/src/test/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// import * as vscode from 'vscode';
|
||||
// import { context } from './testContext';
|
||||
|
||||
const path = require('path');
|
||||
const testRunner = require('vscode/lib/testrunner');
|
||||
|
||||
const suite = 'Notebook Tests';
|
||||
|
||||
const options: any = {
|
||||
ui: 'bdd',
|
||||
useColors: true,
|
||||
timeout: 600000
|
||||
};
|
||||
|
||||
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
|
||||
options.reporter = 'mocha-multi-reporters';
|
||||
options.reporterOptions = {
|
||||
reporterEnabled: 'spec, mocha-junit-reporter',
|
||||
mochaJunitReporterReporterOptions: {
|
||||
testsuitesTitle: `${suite} ${process.platform}`,
|
||||
mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
testRunner.configure(options);
|
||||
|
||||
export = testRunner;
|
||||
97
extensions/notebook/src/test/model/contentManagers.test.ts
Normal file
97
extensions/notebook/src/test/model/contentManagers.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as should from 'should';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import * as path from 'path';
|
||||
import { ContentsManager, Contents } from '@jupyterlab/services';
|
||||
import { nb } from 'sqlops';
|
||||
import 'mocha';
|
||||
|
||||
import { INotebook, CellTypes } from '../../contracts/content';
|
||||
import { RemoteContentManager } from '../../jupyter/remoteContentManager';
|
||||
import * as testUtils from '../common/testUtils';
|
||||
|
||||
let expectedNotebookContent: INotebook = {
|
||||
cells: [{
|
||||
cell_type: CellTypes.Code,
|
||||
source: 'insert into t1 values (c1, c2)',
|
||||
metadata: { language: 'python' },
|
||||
execution_count: 1
|
||||
}],
|
||||
metadata: {
|
||||
kernelspec: {
|
||||
name: 'mssql',
|
||||
language: 'sql'
|
||||
}
|
||||
},
|
||||
nbformat: 5,
|
||||
nbformat_minor: 0
|
||||
};
|
||||
let notebookContentString = JSON.stringify(expectedNotebookContent);
|
||||
|
||||
function verifyMatchesExpectedNotebook(notebook: nb.INotebookContents): void {
|
||||
should(notebook.cells).have.length(1, 'Expected 1 cell');
|
||||
should(notebook.cells[0].cell_type).equal(CellTypes.Code);
|
||||
should(notebook.cells[0].source).equal(expectedNotebookContent.cells[0].source);
|
||||
should(notebook.metadata.kernelspec.name).equal(expectedNotebookContent.metadata.kernelspec.name);
|
||||
should(notebook.nbformat).equal(expectedNotebookContent.nbformat);
|
||||
should(notebook.nbformat_minor).equal(expectedNotebookContent.nbformat_minor);
|
||||
}
|
||||
|
||||
describe('Remote Content Manager', function (): void {
|
||||
let mockJupyterManager = TypeMoq.Mock.ofType(ContentsManager);
|
||||
let contentManager = new RemoteContentManager(mockJupyterManager.object);
|
||||
|
||||
// TODO re-enable when we bring in usage of remote content managers / binders
|
||||
// it('Should return undefined if path is undefined', async function(): Promise<void> {
|
||||
// let content = await contentManager.getNotebookContents(undefined);
|
||||
// should(content).be.undefined();
|
||||
// // tslint:disable-next-line:no-null-keyword
|
||||
// content = await contentManager.getNotebookContents(null);
|
||||
// should(content).be.undefined();
|
||||
// content = await contentManager.getNotebookContents(vscode.Uri.file(''));
|
||||
// should(content).be.undefined();
|
||||
// });
|
||||
|
||||
it('Should throw if API call throws', async function (): Promise<void> {
|
||||
let exception = new Error('Path was wrong');
|
||||
mockJupyterManager.setup(c => c.get(TypeMoq.It.isAny(), TypeMoq.It.isAny())).throws(exception);
|
||||
await testUtils.assertThrowsAsync(async () => await contentManager.getNotebookContents(vscode.Uri.file('/path/doesnot/exist.ipynb')), undefined);
|
||||
});
|
||||
it('Should return notebook contents parsed as INotebook when valid notebook file parsed', async function (): Promise<void> {
|
||||
// Given a valid request to the notebook server
|
||||
let remotePath = '/remote/path/that/exists.ipynb';
|
||||
let contentsModel: Contents.IModel = {
|
||||
name: path.basename(remotePath),
|
||||
content: expectedNotebookContent,
|
||||
path: remotePath,
|
||||
type: 'notebook',
|
||||
writable: false,
|
||||
created: undefined,
|
||||
last_modified: undefined,
|
||||
mimetype: 'json',
|
||||
format: 'json'
|
||||
};
|
||||
mockJupyterManager.setup(c => c.get(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(contentsModel));
|
||||
// when I read the content
|
||||
let notebook = await contentManager.getNotebookContents(vscode.Uri.file(remotePath));
|
||||
// then I expect notebook format to match
|
||||
verifyMatchesExpectedNotebook(notebook);
|
||||
});
|
||||
|
||||
it('Should return undefined if service does not return anything', async function (): Promise<void> {
|
||||
// Given a valid request to the notebook server
|
||||
let remotePath = '/remote/path/that/does/not/exist.ipynb';
|
||||
mockJupyterManager.setup(c => c.get(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined));
|
||||
// when I read the content
|
||||
let notebook = await contentManager.getNotebookContents(vscode.Uri.file(remotePath));
|
||||
// then I expect notebook format to match
|
||||
should(notebook).be.undefined();
|
||||
});
|
||||
});
|
||||
203
extensions/notebook/src/test/model/kernel.test.ts
Normal file
203
extensions/notebook/src/test/model/kernel.test.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as should from 'should';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import { nb } from 'sqlops';
|
||||
import { Kernel, KernelMessage } from '@jupyterlab/services';
|
||||
import 'mocha';
|
||||
|
||||
import { KernelStub, FutureStub } from '../common';
|
||||
import { JupyterKernel, JupyterFuture } from '../../jupyter/jupyterKernel';
|
||||
|
||||
describe('Jupyter Session', function (): void {
|
||||
let mockJupyterKernel: TypeMoq.IMock<KernelStub>;
|
||||
let kernel: JupyterKernel;
|
||||
|
||||
beforeEach(() => {
|
||||
mockJupyterKernel = TypeMoq.Mock.ofType(KernelStub);
|
||||
kernel = new JupyterKernel(mockJupyterKernel.object);
|
||||
});
|
||||
|
||||
it('should pass through most properties', function (done): void {
|
||||
// Given values for the passthrough properties
|
||||
mockJupyterKernel.setup(s => s.id).returns(() => 'id');
|
||||
mockJupyterKernel.setup(s => s.name).returns(() => 'name');
|
||||
mockJupyterKernel.setup(s => s.isReady).returns(() => true);
|
||||
let readyPromise = Promise.reject('err');
|
||||
mockJupyterKernel.setup(s => s.ready).returns(() => readyPromise);
|
||||
// Should return those values when called
|
||||
should(kernel.id).equal('id');
|
||||
should(kernel.name).equal('name');
|
||||
should(kernel.isReady).be.true();
|
||||
|
||||
kernel.ready.then((fulfilled) => done('Err: should not succeed'), (err) => done());
|
||||
});
|
||||
|
||||
it('should passthrough spec with expected name and display name', async function (): Promise<void> {
|
||||
let spec: Kernel.ISpecModel = {
|
||||
name: 'python',
|
||||
display_name: 'Python 3',
|
||||
language: 'python',
|
||||
argv: undefined,
|
||||
resources: undefined
|
||||
};
|
||||
mockJupyterKernel.setup(k => k.getSpec()).returns(() => Promise.resolve(spec));
|
||||
|
||||
let actualSpec = await kernel.getSpec();
|
||||
should(actualSpec.name).equal('python');
|
||||
should(actualSpec.display_name).equal('Python 3');
|
||||
});
|
||||
|
||||
it('should return code completions on requestComplete', async function (): Promise<void> {
|
||||
should(kernel.supportsIntellisense).be.true();
|
||||
let completeMsg: KernelMessage.ICompleteReplyMsg = {
|
||||
channel: 'shell',
|
||||
content: {
|
||||
cursor_start: 0,
|
||||
cursor_end: 2,
|
||||
matches: ['print'],
|
||||
metadata: {},
|
||||
status: 'ok'
|
||||
},
|
||||
header: undefined,
|
||||
metadata: undefined,
|
||||
parent_header: undefined
|
||||
};
|
||||
mockJupyterKernel.setup(k => k.requestComplete(TypeMoq.It.isAny())).returns(() => Promise.resolve(completeMsg));
|
||||
|
||||
let msg = await kernel.requestComplete({
|
||||
code: 'pr',
|
||||
cursor_pos: 2
|
||||
});
|
||||
should(msg.type).equal('shell');
|
||||
should(msg.content).equal(completeMsg.content);
|
||||
});
|
||||
|
||||
it('should return a simple future on requestExecute', async function (): Promise<void> {
|
||||
let futureMock = TypeMoq.Mock.ofType(FutureStub);
|
||||
const code = 'print("hello")';
|
||||
let msg: KernelMessage.IShellMessage = {
|
||||
channel: 'shell',
|
||||
content: { code: code },
|
||||
header: undefined,
|
||||
metadata: undefined,
|
||||
parent_header: undefined
|
||||
};
|
||||
futureMock.setup(f => f.msg).returns(() => msg);
|
||||
let executeRequest: KernelMessage.IExecuteRequest;
|
||||
let shouldDispose: KernelMessage.IExecuteRequest;
|
||||
mockJupyterKernel.setup(k => k.requestExecute(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((request, disposeOnDone) => {
|
||||
executeRequest = request;
|
||||
shouldDispose = disposeOnDone;
|
||||
return futureMock.object;
|
||||
});
|
||||
|
||||
// When I request execute
|
||||
let future = kernel.requestExecute({
|
||||
code: code
|
||||
}, true);
|
||||
|
||||
// Then expect wrapper to be returned
|
||||
should(future).be.instanceof(JupyterFuture);
|
||||
should(future.msg.type).equal('shell');
|
||||
should(future.msg.content.code).equal(code);
|
||||
should(executeRequest.code).equal(code);
|
||||
should(shouldDispose).be.true();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Jupyter Future', function (): void {
|
||||
let mockJupyterFuture: TypeMoq.IMock<FutureStub>;
|
||||
let future: JupyterFuture;
|
||||
|
||||
beforeEach(() => {
|
||||
mockJupyterFuture = TypeMoq.Mock.ofType(FutureStub);
|
||||
future = new JupyterFuture(mockJupyterFuture.object);
|
||||
});
|
||||
|
||||
it('should return message on done', async function (): Promise<void> {
|
||||
let msg: KernelMessage.IShellMessage = {
|
||||
channel: 'shell',
|
||||
content: { code: 'exec' },
|
||||
header: undefined,
|
||||
metadata: undefined,
|
||||
parent_header: undefined
|
||||
};
|
||||
|
||||
mockJupyterFuture.setup(f => f.done).returns(() => Promise.resolve(msg));
|
||||
|
||||
let actualMsg = await future.done;
|
||||
should(actualMsg.content.code).equal('exec');
|
||||
});
|
||||
|
||||
it('should relay reply message', async function (): Promise<void> {
|
||||
let handler: (msg: KernelMessage.IShellMessage) => void | PromiseLike<void>;
|
||||
mockJupyterFuture.setup(f => f.onReply = TypeMoq.It.isAny()).callback(h => handler = h);
|
||||
|
||||
// When I set a reply handler and a message is sent
|
||||
let msg: nb.IShellMessage;
|
||||
future.setReplyHandler({
|
||||
handle: (message => {
|
||||
msg = message;
|
||||
})
|
||||
});
|
||||
should(handler).not.be.undefined();
|
||||
verifyRelayMessage('shell', handler, () => msg);
|
||||
|
||||
});
|
||||
|
||||
it('should relay StdIn message', async function (): Promise<void> {
|
||||
let handler: (msg: KernelMessage.IStdinMessage) => void | PromiseLike<void>;
|
||||
mockJupyterFuture.setup(f => f.onStdin = TypeMoq.It.isAny()).callback(h => handler = h);
|
||||
|
||||
// When I set a reply handler and a message is sent
|
||||
let msg: nb.IStdinMessage;
|
||||
future.setStdInHandler({
|
||||
handle: (message => {
|
||||
msg = message;
|
||||
})
|
||||
});
|
||||
should(handler).not.be.undefined();
|
||||
verifyRelayMessage('stdin', handler, () => msg);
|
||||
});
|
||||
|
||||
it('should relay IOPub message', async function (): Promise<void> {
|
||||
let handler: (msg: KernelMessage.IIOPubMessage) => void | PromiseLike<void>;
|
||||
mockJupyterFuture.setup(f => f.onIOPub = TypeMoq.It.isAny()).callback(h => handler = h);
|
||||
|
||||
// When I set a reply handler and a message is sent
|
||||
let msg: nb.IIOPubMessage;
|
||||
future.setIOPubHandler({
|
||||
handle: (message => {
|
||||
msg = message;
|
||||
})
|
||||
});
|
||||
should(handler).not.be.undefined();
|
||||
verifyRelayMessage('iopub', handler, () => msg);
|
||||
});
|
||||
|
||||
function verifyRelayMessage(channel: nb.Channel | KernelMessage.Channel, handler: (msg: KernelMessage.IMessage) => void | PromiseLike<void>, getMessage: () => nb.IMessage): void {
|
||||
handler({
|
||||
channel: <any>channel,
|
||||
content: { value: 'test' },
|
||||
metadata: { value: 'test' },
|
||||
header: { username: 'test', version: '1', msg_id: undefined, msg_type: undefined, session: undefined },
|
||||
parent_header: { username: 'test', version: '1', msg_id: undefined, msg_type: undefined, session: undefined }
|
||||
});
|
||||
let msg = getMessage();
|
||||
// Then the value should be relayed
|
||||
should(msg.type).equal(channel);
|
||||
should(msg.content).have.property('value', 'test');
|
||||
should(msg.metadata).have.property('value', 'test');
|
||||
should(msg.header).have.property('username', 'test');
|
||||
should(msg.parent_header).have.property('username', 'test');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
236
extensions/notebook/src/test/model/serverInstance.test.ts
Normal file
236
extensions/notebook/src/test/model/serverInstance.test.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as should from 'should';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import * as vscode from 'vscode';
|
||||
import * as stream from 'stream';
|
||||
import { ChildProcess } from 'child_process';
|
||||
import 'mocha';
|
||||
|
||||
import JupyterServerInstallation from '../../jupyter/jupyterServerInstallation';
|
||||
import { ApiWrapper } from '../..//common/apiWrapper';
|
||||
import { PerNotebookServerInstance, ServerInstanceUtils } from '../../jupyter/serverInstance';
|
||||
import { MockOutputChannel } from '../common/stubs';
|
||||
import * as testUtils from '../common/testUtils';
|
||||
import { LocalJupyterServerManager } from '../../jupyter/jupyterServerManager';
|
||||
|
||||
const successMessage = `[I 14:00:38.811 NotebookApp] The Jupyter Notebook is running at:
|
||||
[I 14:00:38.812 NotebookApp] http://localhost:8891/?token=...
|
||||
[I 14:00:38.812 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
|
||||
`;
|
||||
|
||||
describe('Jupyter server instance', function (): void {
|
||||
let expectedPath = 'mydir/notebook.ipynb';
|
||||
let mockInstall: TypeMoq.IMock<JupyterServerInstallation>;
|
||||
let mockOutputChannel: TypeMoq.IMock<MockOutputChannel>;
|
||||
let mockApiWrapper: TypeMoq.IMock<ApiWrapper>;
|
||||
let mockUtils: TypeMoq.IMock<ServerInstanceUtils>;
|
||||
let serverInstance: PerNotebookServerInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
mockApiWrapper = TypeMoq.Mock.ofType(ApiWrapper);
|
||||
mockApiWrapper.setup(a => a.showErrorMessage(TypeMoq.It.isAny()));
|
||||
mockApiWrapper.setup(a => a.getWorkspacePathFromUri(TypeMoq.It.isAny())).returns(() => undefined);
|
||||
mockInstall = TypeMoq.Mock.ofType(JupyterServerInstallation, undefined, undefined, '/root');
|
||||
mockOutputChannel = TypeMoq.Mock.ofType(MockOutputChannel);
|
||||
mockInstall.setup(i => i.outputChannel).returns(() => mockOutputChannel.object);
|
||||
mockInstall.setup(i => i.pythonExecutable).returns(() => 'python3');
|
||||
mockUtils = TypeMoq.Mock.ofType(ServerInstanceUtils);
|
||||
mockUtils.setup(u => u.checkProcessDied(TypeMoq.It.isAny())).returns(() => undefined);
|
||||
serverInstance = new PerNotebookServerInstance({
|
||||
documentPath: expectedPath,
|
||||
install: mockInstall.object
|
||||
}, mockUtils.object);
|
||||
});
|
||||
|
||||
|
||||
it('Should not be started initially', function (): void {
|
||||
// Given a new instance It should not be started
|
||||
should(serverInstance.isStarted).be.false();
|
||||
should(serverInstance.port).be.undefined();
|
||||
});
|
||||
|
||||
it('Should create config and data directories on configure', async function (): Promise<void> {
|
||||
// Given a server instance
|
||||
mockUtils.setup(u => u.mkDir(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => Promise.resolve());
|
||||
mockUtils.setup(u => u.copy(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns(() => Promise.resolve());
|
||||
mockUtils.setup(u => u.existsSync(TypeMoq.It.isAnyString())).returns(() => false);
|
||||
|
||||
// When I run configure
|
||||
await serverInstance.configure();
|
||||
|
||||
// Then I expect a folder to have been created with config and data subdirs
|
||||
mockUtils.verify(u => u.mkDir(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(5));
|
||||
mockUtils.verify(u => u.copy(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(3));
|
||||
mockUtils.verify(u => u.existsSync(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(1));
|
||||
});
|
||||
|
||||
it('Should have URI info after start', async function (): Promise<void> {
|
||||
// Given startup will succeed
|
||||
let process = setupSpawn({
|
||||
sdtout: (listener) => undefined,
|
||||
stderr: (listener) => listener(successMessage)
|
||||
});
|
||||
mockUtils.setup(u => u.spawn(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.returns(() => <ChildProcess>process.object);
|
||||
|
||||
// When I call start
|
||||
await serverInstance.start();
|
||||
|
||||
// Then I expect all parts of the URI to be defined
|
||||
should(serverInstance.uri).not.be.undefined();
|
||||
should(serverInstance.uri.scheme).equal('http');
|
||||
let settings = LocalJupyterServerManager.getLocalConnectionSettings(serverInstance.uri);
|
||||
// Verify a token with expected length was generated
|
||||
should(settings.token).have.length(48);
|
||||
let hostAndPort = serverInstance.uri.authority.split(':');
|
||||
// verify port was set as expected
|
||||
should(hostAndPort[1]).length(4);
|
||||
|
||||
// And I expect it to be started
|
||||
should(serverInstance.isStarted).be.true();
|
||||
|
||||
// And I expect listeners to be cleaned up
|
||||
process.verify(p => p.on(TypeMoq.It.isValue('error'), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
process.verify(p => p.on(TypeMoq.It.isValue('exit'), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
});
|
||||
|
||||
it('Should throw if error before startup', async function (): Promise<void> {
|
||||
let error = 'myerr';
|
||||
let process = setupSpawn({
|
||||
sdtout: (listener) => undefined,
|
||||
stderr: (listener) => listener(successMessage),
|
||||
error: (listener) => setTimeout(() => listener(new Error(error)), 10)
|
||||
});
|
||||
mockUtils.setup(u => u.spawn(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.returns(() => <ChildProcess>process.object);
|
||||
|
||||
// When I call start then I expect it to pass
|
||||
await serverInstance.start();
|
||||
});
|
||||
|
||||
it('Should throw if exit before startup', async function (): Promise<void> {
|
||||
let code = -1234;
|
||||
let process = setupSpawn({
|
||||
exit: (listener) => listener(code)
|
||||
});
|
||||
mockUtils.setup(u => u.spawn(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.returns(() => <ChildProcess>process.object);
|
||||
|
||||
// When I call start then I expect the error to be thrown
|
||||
await testUtils.assertThrowsAsync(() => serverInstance.start(), undefined);
|
||||
should(serverInstance.isStarted).be.false();
|
||||
});
|
||||
|
||||
it('Should call stop with correct port on close', async function (): Promise<void> {
|
||||
// Given startup will succeed
|
||||
let process = setupSpawn({
|
||||
sdtout: (listener) => undefined,
|
||||
stderr: (listener) => listener(successMessage)
|
||||
});
|
||||
mockUtils.setup(u => u.spawn(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.returns(() => <ChildProcess>process.object);
|
||||
|
||||
let actualCommand: string = undefined;
|
||||
mockUtils.setup(u => u.executeBufferedCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.returns((cmd) => {
|
||||
actualCommand = cmd;
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
mockUtils.setup(u => u.pathExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(false));
|
||||
mockUtils.setup(u => u.removeDir(TypeMoq.It.isAny())).returns(() => Promise.resolve());
|
||||
// When I call start and then stop
|
||||
await serverInstance.start();
|
||||
await serverInstance.stop();
|
||||
|
||||
// Then I expect stop to be called on the child process
|
||||
should(actualCommand.indexOf(`jupyter notebook stop ${serverInstance.port}`)).be.greaterThan(-1);
|
||||
mockUtils.verify(u => u.removeDir(TypeMoq.It.isAny()), TypeMoq.Times.never());
|
||||
});
|
||||
|
||||
it('Should remove directory on close', async function (): Promise<void> {
|
||||
// Given configure and startup are done
|
||||
mockUtils.setup(u => u.mkDir(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => Promise.resolve());
|
||||
mockUtils.setup(u => u.copy(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns(() => Promise.resolve());
|
||||
|
||||
let process = setupSpawn({
|
||||
sdtout: (listener) => undefined,
|
||||
stderr: (listener) => listener(successMessage)
|
||||
});
|
||||
mockUtils.setup(u => u.spawn(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.returns(() => <ChildProcess>process.object);
|
||||
mockUtils.setup(u => u.executeBufferedCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.returns((cmd) => Promise.resolve(undefined));
|
||||
mockUtils.setup(u => u.pathExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(true));
|
||||
mockUtils.setup(u => u.removeDir(TypeMoq.It.isAny())).returns(() => Promise.resolve());
|
||||
|
||||
await serverInstance.configure();
|
||||
await serverInstance.start();
|
||||
|
||||
// When I call stop
|
||||
await serverInstance.stop();
|
||||
|
||||
// Then I expect the directory to be cleaned up
|
||||
mockUtils.verify(u => u.removeDir(TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
});
|
||||
|
||||
function setupSpawn(callbacks: IProcessCallbacks): TypeMoq.IMock<ChildProcessStub> {
|
||||
|
||||
let stdoutMock = TypeMoq.Mock.ofType(stream.Readable);
|
||||
stdoutMock.setup(s => s.on(TypeMoq.It.isValue('data'), TypeMoq.It.isAny()))
|
||||
.returns((event, listener) => runIfExists(listener, callbacks.sdtout));
|
||||
let stderrMock = TypeMoq.Mock.ofType(stream.Readable);
|
||||
stderrMock.setup(s => s.on(TypeMoq.It.isValue('data'), TypeMoq.It.isAny()))
|
||||
.returns((event, listener) => runIfExists(listener, callbacks.stderr));
|
||||
let mockProcess = TypeMoq.Mock.ofType(ChildProcessStub);
|
||||
mockProcess.setup(p => p.stdout).returns(() => stdoutMock.object);
|
||||
mockProcess.setup(p => p.stderr).returns(() => stderrMock.object);
|
||||
mockProcess.setup(p => p.on(TypeMoq.It.isValue('exit'), TypeMoq.It.isAny()))
|
||||
.returns((event, listener) => runIfExists(listener, callbacks.exit));
|
||||
mockProcess.setup(p => p.on(TypeMoq.It.isValue('error'), TypeMoq.It.isAny()))
|
||||
.returns((event, listener) => runIfExists(listener, callbacks.error));
|
||||
mockProcess.setup(p => p.removeListener(TypeMoq.It.isAny(), TypeMoq.It.isAny()));
|
||||
mockProcess.setup(p => p.addListener(TypeMoq.It.isAny(), TypeMoq.It.isAny()));
|
||||
return mockProcess;
|
||||
}
|
||||
|
||||
function runIfExists(listener: any, callback: Function, delay: number = 5): stream.Readable {
|
||||
setTimeout(() => {
|
||||
if (callback) {
|
||||
callback(listener);
|
||||
}
|
||||
}, delay);
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
interface IProcessCallbacks {
|
||||
sdtout?: Function;
|
||||
stderr?: Function;
|
||||
exit?: Function;
|
||||
error?: Function;
|
||||
}
|
||||
|
||||
class ChildProcessStub {
|
||||
public get stdout(): stream.Readable {
|
||||
return undefined;
|
||||
}
|
||||
public get stderr(): stream.Readable {
|
||||
return undefined;
|
||||
}
|
||||
// tslint:disable-next-line:typedef
|
||||
on(event: any, listener: any) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
addListener(event: string, listener: Function): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
removeListener(event: string, listener: Function): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
123
extensions/notebook/src/test/model/serverManager.test.ts
Normal file
123
extensions/notebook/src/test/model/serverManager.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as should from 'should';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import * as vscode from 'vscode';
|
||||
import 'mocha';
|
||||
|
||||
import { JupyterServerInstanceStub } from '../common';
|
||||
import { LocalJupyterServerManager, ServerInstanceFactory } from '../../jupyter/jupyterServerManager';
|
||||
import JupyterServerInstallation from '../../jupyter/jupyterServerInstallation';
|
||||
import { Deferred } from '../../common/promise';
|
||||
import { ApiWrapper } from '../../common/apiWrapper';
|
||||
import * as testUtils from '../common/testUtils';
|
||||
import { IServerInstance } from '../../jupyter/common';
|
||||
import { MockExtensionContext } from '../common/stubs';
|
||||
|
||||
describe('Local Jupyter Server Manager', function (): void {
|
||||
let expectedPath = 'my/notebook.ipynb';
|
||||
let serverManager: LocalJupyterServerManager;
|
||||
let deferredInstall: Deferred<JupyterServerInstallation>;
|
||||
let mockApiWrapper: TypeMoq.IMock<ApiWrapper>;
|
||||
let mockExtensionContext: MockExtensionContext;
|
||||
let mockFactory: TypeMoq.IMock<ServerInstanceFactory>;
|
||||
beforeEach(() => {
|
||||
mockExtensionContext = new MockExtensionContext();
|
||||
mockApiWrapper = TypeMoq.Mock.ofType(ApiWrapper);
|
||||
mockApiWrapper.setup(a => a.showErrorMessage(TypeMoq.It.isAny()));
|
||||
mockApiWrapper.setup(a => a.getWorkspacePathFromUri(TypeMoq.It.isAny())).returns(() => undefined);
|
||||
mockFactory = TypeMoq.Mock.ofType(ServerInstanceFactory);
|
||||
deferredInstall = new Deferred<JupyterServerInstallation>();
|
||||
serverManager = new LocalJupyterServerManager({
|
||||
documentPath: expectedPath,
|
||||
jupyterInstallation: deferredInstall.promise,
|
||||
extensionContext: mockExtensionContext,
|
||||
apiWrapper: mockApiWrapper.object,
|
||||
factory: mockFactory.object
|
||||
});
|
||||
});
|
||||
|
||||
it('Should not be started initially', function (): void {
|
||||
should(serverManager.isStarted).be.false();
|
||||
should(serverManager.serverSettings).be.undefined();
|
||||
});
|
||||
|
||||
it('Should show error message on install failure', async function (): Promise<void> {
|
||||
let error = 'Error!!';
|
||||
deferredInstall.reject(error);
|
||||
await testUtils.assertThrowsAsync(() => serverManager.startServer(), undefined);
|
||||
mockApiWrapper.verify(a => a.showErrorMessage(TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
});
|
||||
|
||||
it('Should configure and start install', async function (): Promise<void> {
|
||||
// Given an install and instance that start with no issues
|
||||
let expectedUri = vscode.Uri.parse('http://localhost:1234?token=abcdefghijk');
|
||||
let [mockInstall, mockServerInstance] = initInstallAndInstance(expectedUri);
|
||||
deferredInstall.resolve(mockInstall.object);
|
||||
|
||||
// When I start the server
|
||||
let notified = false;
|
||||
serverManager.onServerStarted(() => notified = true);
|
||||
await serverManager.startServer();
|
||||
|
||||
// Then I expect the port to be included in settings
|
||||
should(serverManager.serverSettings.baseUrl.indexOf('1234') > -1).be.true();
|
||||
should(serverManager.serverSettings.token).equal('abcdefghijk');
|
||||
// And a notification to be sent
|
||||
should(notified).be.true();
|
||||
// And the key methods to have been called
|
||||
mockServerInstance.verify(s => s.configure(), TypeMoq.Times.once());
|
||||
mockServerInstance.verify(s => s.start(), TypeMoq.Times.once());
|
||||
});
|
||||
|
||||
it('Should not fail on stop if never started', async function (): Promise<void> {
|
||||
await serverManager.stopServer();
|
||||
});
|
||||
|
||||
it('Should call stop on server instance', async function (): Promise<void> {
|
||||
// Given an install and instance that start with no issues
|
||||
let expectedUri = vscode.Uri.parse('http://localhost:1234?token=abcdefghijk');
|
||||
let [mockInstall, mockServerInstance] = initInstallAndInstance(expectedUri);
|
||||
mockServerInstance.setup(s => s.stop()).returns(() => Promise.resolve());
|
||||
deferredInstall.resolve(mockInstall.object);
|
||||
|
||||
// When I start and then the server
|
||||
await serverManager.startServer();
|
||||
await serverManager.stopServer();
|
||||
|
||||
// Then I expect stop to have been called on the server instance
|
||||
mockServerInstance.verify(s => s.stop(), TypeMoq.Times.once());
|
||||
});
|
||||
|
||||
it('Should call stop when extension is disposed', async function (): Promise<void> {
|
||||
// Given an install and instance that start with no issues
|
||||
let expectedUri = vscode.Uri.parse('http://localhost:1234?token=abcdefghijk');
|
||||
let [mockInstall, mockServerInstance] = initInstallAndInstance(expectedUri);
|
||||
mockServerInstance.setup(s => s.stop()).returns(() => Promise.resolve());
|
||||
deferredInstall.resolve(mockInstall.object);
|
||||
|
||||
// When I start and then dispose the extension
|
||||
await serverManager.startServer();
|
||||
should(mockExtensionContext.subscriptions).have.length(1);
|
||||
mockExtensionContext.subscriptions[0].dispose();
|
||||
|
||||
// Then I expect stop to have been called on the server instance
|
||||
mockServerInstance.verify(s => s.stop(), TypeMoq.Times.once());
|
||||
});
|
||||
|
||||
function initInstallAndInstance(uri: vscode.Uri): [TypeMoq.IMock<JupyterServerInstallation>, TypeMoq.IMock<IServerInstance>] {
|
||||
let mockInstall = TypeMoq.Mock.ofType(JupyterServerInstallation, undefined, undefined, '/root');
|
||||
let mockServerInstance = TypeMoq.Mock.ofType(JupyterServerInstanceStub);
|
||||
mockFactory.setup(f => f.createInstance(TypeMoq.It.isAny())).returns(() => mockServerInstance.object);
|
||||
mockServerInstance.setup(s => s.configure()).returns(() => Promise.resolve());
|
||||
mockServerInstance.setup(s => s.start()).returns(() => Promise.resolve());
|
||||
mockServerInstance.setup(s => s.uri).returns(() => uri);
|
||||
return [mockInstall, mockServerInstance];
|
||||
}
|
||||
});
|
||||
171
extensions/notebook/src/test/model/sessionManager.test.ts
Normal file
171
extensions/notebook/src/test/model/sessionManager.test.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as should from 'should';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import { nb } from 'sqlops';
|
||||
import { SessionManager, Session, Kernel } from '@jupyterlab/services';
|
||||
import 'mocha';
|
||||
|
||||
import { JupyterSessionManager, JupyterSession } from '../../jupyter/jupyterSessionManager';
|
||||
import { Deferred } from '../../common/promise';
|
||||
import { SessionStub, KernelStub } from '../common';
|
||||
|
||||
describe('Jupyter Session Manager', function (): void {
|
||||
let mockJupyterManager = TypeMoq.Mock.ofType<SessionManager>();
|
||||
let sessionManager = new JupyterSessionManager();
|
||||
|
||||
it('isReady should only be true after ready promise completes', function (done): void {
|
||||
// Given
|
||||
let deferred = new Deferred<void>();
|
||||
mockJupyterManager.setup(m => m.ready).returns(() => deferred.promise);
|
||||
|
||||
// When I call before resolve I expect it'll be false
|
||||
sessionManager.setJupyterSessionManager(mockJupyterManager.object);
|
||||
should(sessionManager.isReady).be.false();
|
||||
|
||||
// When I call a after resolve, it'll be true
|
||||
deferred.resolve();
|
||||
sessionManager.ready.then(() => {
|
||||
should(sessionManager.isReady).be.true();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should passthrough the ready calls', function (done): void {
|
||||
// Given
|
||||
let deferred = new Deferred<void>();
|
||||
mockJupyterManager.setup(m => m.ready).returns(() => deferred.promise);
|
||||
|
||||
// When I wait on the ready method before completing
|
||||
sessionManager.setJupyterSessionManager(mockJupyterManager.object);
|
||||
sessionManager.ready.then(() => done());
|
||||
|
||||
// Then session manager should eventually resolve
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
it('should handle null specs', function (): void {
|
||||
mockJupyterManager.setup(m => m.specs).returns(() => undefined);
|
||||
let specs = sessionManager.specs;
|
||||
should(specs).be.undefined();
|
||||
});
|
||||
|
||||
it('should map specs to named kernels', function (): void {
|
||||
let internalSpecs: Kernel.ISpecModels = {
|
||||
default: 'mssql',
|
||||
kernelspecs: {
|
||||
'mssql': <Kernel.ISpecModel>{ language: 'sql' },
|
||||
'python': <Kernel.ISpecModel>{ language: 'python' }
|
||||
}
|
||||
};
|
||||
mockJupyterManager.setup(m => m.specs).returns(() => internalSpecs);
|
||||
let specs = sessionManager.specs;
|
||||
should(specs.defaultKernel).equal('mssql');
|
||||
should(specs.kernels).have.length(2);
|
||||
});
|
||||
|
||||
|
||||
it('Should call to startSession with correct params', async function (): Promise<void> {
|
||||
// Given a session request that will complete OK
|
||||
let sessionOptions: nb.ISessionOptions = { path: 'mypath.ipynb' };
|
||||
let expectedSessionInfo = <Session.ISession>{
|
||||
path: sessionOptions.path,
|
||||
id: 'id',
|
||||
name: 'sessionName',
|
||||
type: 'type',
|
||||
kernel: {
|
||||
name: 'name'
|
||||
}
|
||||
};
|
||||
mockJupyterManager.setup(m => m.startNew(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedSessionInfo));
|
||||
|
||||
// When I call startSession
|
||||
let session = await sessionManager.startNew(sessionOptions);
|
||||
// Then I expect the parameters passed to be correct
|
||||
should(session.path).equal(sessionOptions.path);
|
||||
should(session.canChangeKernels).be.true();
|
||||
should(session.id).equal(expectedSessionInfo.id);
|
||||
should(session.name).equal(expectedSessionInfo.name);
|
||||
should(session.type).equal(expectedSessionInfo.type);
|
||||
should(session.kernel.name).equal(expectedSessionInfo.kernel.name);
|
||||
});
|
||||
|
||||
it('Should call to shutdown with correct id', async function (): Promise<void> {
|
||||
let id = 'session1';
|
||||
mockJupyterManager.setup(m => m.shutdown(TypeMoq.It.isValue(id))).returns(() => Promise.resolve());
|
||||
mockJupyterManager.setup(m => m.isDisposed).returns(() => false);
|
||||
await sessionManager.shutdown(id);
|
||||
mockJupyterManager.verify(m => m.shutdown(TypeMoq.It.isValue(id)), TypeMoq.Times.once());
|
||||
});
|
||||
});
|
||||
|
||||
describe('Jupyter Session', function (): void {
|
||||
let mockJupyterSession: TypeMoq.IMock<SessionStub>;
|
||||
let session: JupyterSession;
|
||||
|
||||
beforeEach(() => {
|
||||
mockJupyterSession = TypeMoq.Mock.ofType(SessionStub);
|
||||
session = new JupyterSession(mockJupyterSession.object);
|
||||
});
|
||||
|
||||
it('should always be able to change kernels', function (): void {
|
||||
should(session.canChangeKernels).be.true();
|
||||
});
|
||||
it('should pass through most properties', function (): void {
|
||||
// Given values for the passthrough properties
|
||||
mockJupyterSession.setup(s => s.id).returns(() => 'id');
|
||||
mockJupyterSession.setup(s => s.name).returns(() => 'name');
|
||||
mockJupyterSession.setup(s => s.path).returns(() => 'path');
|
||||
mockJupyterSession.setup(s => s.type).returns(() => 'type');
|
||||
mockJupyterSession.setup(s => s.status).returns(() => 'starting');
|
||||
// Should return those values when called
|
||||
should(session.id).equal('id');
|
||||
should(session.name).equal('name');
|
||||
should(session.path).equal('path');
|
||||
should(session.type).equal('type');
|
||||
should(session.status).equal('starting');
|
||||
});
|
||||
|
||||
it('should handle null kernel', function (): void {
|
||||
mockJupyterSession.setup(s => s.kernel).returns(() => undefined);
|
||||
should(session.kernel).be.undefined();
|
||||
});
|
||||
|
||||
it('should passthrough kernel', function (): void {
|
||||
// Given a kernel with an ID
|
||||
let kernelMock = TypeMoq.Mock.ofType(KernelStub);
|
||||
kernelMock.setup(k => k.id).returns(() => 'id');
|
||||
mockJupyterSession.setup(s => s.kernel).returns(() => kernelMock.object);
|
||||
|
||||
// When I get a wrapper for the kernel
|
||||
let kernel = session.kernel;
|
||||
kernel = session.kernel;
|
||||
// Then I expect it to have the ID, and only be called once
|
||||
should(kernel.id).equal('id');
|
||||
mockJupyterSession.verify(s => s.kernel, TypeMoq.Times.once());
|
||||
});
|
||||
|
||||
it('should send name in changeKernel request', async function (): Promise<void> {
|
||||
// Given change kernel returns something
|
||||
let kernelMock = TypeMoq.Mock.ofType(KernelStub);
|
||||
kernelMock.setup(k => k.id).returns(() => 'id');
|
||||
let options: Partial<Kernel.IModel>;
|
||||
mockJupyterSession.setup(s => s.changeKernel(TypeMoq.It.isAny())).returns((opts) => {
|
||||
options = opts;
|
||||
return Promise.resolve(kernelMock.object);
|
||||
});
|
||||
|
||||
// When I call changeKernel on the wrapper
|
||||
let kernel = await session.changeKernel({
|
||||
name: 'python'
|
||||
});
|
||||
// Then I expect it to have the ID, and only be called once
|
||||
should(kernel.id).equal('id');
|
||||
should(options.name).equal('python');
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@
|
||||
"target": "es6",
|
||||
"outDir": "./out",
|
||||
"lib": [
|
||||
"es6", "es2015.promise"
|
||||
"dom", "es6", "es2015.promise"
|
||||
],
|
||||
"typeRoots": [
|
||||
"./node_modules/@types"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
0
scripts/sql-test-integration.sh
Normal file → Executable file
0
scripts/sql-test-integration.sh
Normal file → Executable file
22
scripts/test-extensions.sh
Executable file
22
scripts/test-extensions.sh
Executable 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
|
||||
Reference in New Issue
Block a user