diff --git a/build/webpack/common.js b/build/webpack/common.js index 02e77ae61354..8150caebf015 100644 --- a/build/webpack/common.js +++ b/build/webpack/common.js @@ -35,7 +35,8 @@ exports.nodeModulesToExternalize = [ '@koa/cors', 'koa', 'koa-compress', - 'koa-logger' + 'koa-logger', + 'zeromq' ]; exports.nodeModulesToReplacePaths = [...exports.nodeModulesToExternalize]; function getDefaultPlugins(name) { diff --git a/build/webpack/webpack.extension.config.js b/build/webpack/webpack.extension.config.js index 54e7c49ef16d..26ba0fb968d3 100644 --- a/build/webpack/webpack.extension.config.js +++ b/build/webpack/webpack.extension.config.js @@ -2,6 +2,8 @@ // Licensed under the MIT License. 'use strict'; +const copyWebpackPlugin = require('copy-webpack-plugin'); +const removeFilesWebpackPlugin = require('remove-files-webpack-plugin'); const path = require('path'); const tsconfig_paths_webpack_plugin = require('tsconfig-paths-webpack-plugin'); const constants = require('../constants'); @@ -76,7 +78,15 @@ const config = { ] } ] - }) + }), + // ZMQ requires prebuilds to be in our node_modules directory. So recreate the ZMQ structure. + // However we don't webpack to manage this, so it was part of the excluded modules. Delete it from there + // so at runtime we pick up the original structure. + new removeFilesWebpackPlugin({ after: { include: ['./out/client/node_modules/zeromq.js'] } }), + new copyWebpackPlugin([{ from: './node_modules/zeromq/**/*.js' }]), + new copyWebpackPlugin([{ from: './node_modules/zeromq/**/*.node' }]), + new copyWebpackPlugin([{ from: './node_modules/zeromq/**/*.json' }]), + new copyWebpackPlugin([{ from: './node_modules/node-gyp-build/**/*' }]) ], resolve: { alias: { diff --git a/gulpfile.js b/gulpfile.js index e03634a86e5b..66bed78babbd 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -709,6 +709,7 @@ function hasNativeDependencies() { path.dirname(item.substring(item.indexOf('node_modules') + 'node_modules'.length)).split(path.sep) ) .filter(item => item.length > 0) + .filter(item => !item.includes('zeromq')) // This is a known native. Allow this one for now .filter( item => jsonProperties.findIndex(flattenedDependency => diff --git a/news/3 Code Health/10483.md b/news/3 Code Health/10483.md new file mode 100644 index 000000000000..d86f97481b94 --- /dev/null +++ b/news/3 Code Health/10483.md @@ -0,0 +1 @@ +Add ZMQ library to extension \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 98c779f8b317..8f321226aa25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1732,6 +1732,45 @@ "integrity": "sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q==", "dev": true }, + "@sindresorhus/df": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/df/-/df-2.1.0.tgz", + "integrity": "sha1-0gjPJ+BvC7R20U197M19cm6ao4k=", + "dev": true, + "requires": { + "execa": "^0.2.2" + }, + "dependencies": { + "execa": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.2.2.tgz", + "integrity": "sha1-4urUcsLDGq1vc/GslW7vReEjIMs=", + "dev": true, + "requires": { + "cross-spawn-async": "^2.1.1", + "npm-run-path": "^1.0.0", + "object-assign": "^4.0.1", + "path-key": "^1.0.0", + "strip-eof": "^1.0.0" + } + }, + "npm-run-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-1.0.0.tgz", + "integrity": "sha1-9cMr9ZX+ga6Sfa7FLoL4sACsPI8=", + "dev": true, + "requires": { + "path-key": "^1.0.0" + } + }, + "path-key": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-1.0.0.tgz", + "integrity": "sha1-XVPVeAGWRsDWiADbThRua9wqx68=", + "dev": true + } + } + }, "@sindresorhus/is": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", @@ -1774,6 +1813,12 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, + "@stroncium/procfs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@stroncium/procfs/-/procfs-1.1.1.tgz", + "integrity": "sha512-c8A7hTAu1FgWJTfOQNqp0N9K/9amlVktNRQidzeeX4dTJpSNl2Y65s9BSfnMlFFGMGP+rBvrb0WTTv7ad6MJ9A==", + "dev": true + }, "@testing-library/dom": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-6.11.0.tgz", @@ -2510,6 +2555,12 @@ "integrity": "sha512-EZQUP3hSZQyTQRfiLqelC9NMWd1kqLcmQE0dMiklxBkgi84T+cHOhnKpgk4NnOWpGX863yE6+IaGnOXUNFqDnQ==", "dev": true }, + "@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", + "dev": true + }, "@types/stack-trace": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/stack-trace/-/stack-trace-0.0.29.tgz", @@ -2624,6 +2675,17 @@ "@types/webpack": "*" } }, + "@types/webpack-sources": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.6.tgz", + "integrity": "sha512-FtAWR7wR5ocJ9+nP137DV81tveD/ZgB1sadnJ/axUGM3BUVfRPx8oQNMtv3JNfTeHx3VP7cXiyfR/jmtEsVHsQ==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.6.1" + } + }, "@types/winreg": { "version": "1.2.30", "resolved": "https://registry.npmjs.org/@types/winreg/-/winreg-1.2.30.tgz", @@ -5687,6 +5749,19 @@ "parse-json": "^4.0.0" } }, + "cp-file": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-6.2.0.tgz", + "integrity": "sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "make-dir": "^2.0.0", + "nested-error-stacks": "^2.0.0", + "pify": "^4.0.1", + "safe-buffer": "^5.0.1" + } + }, "cpx": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/cpx/-/cpx-1.5.0.tgz", @@ -6020,6 +6095,16 @@ "which": "^1.2.9" } }, + "cross-spawn-async": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz", + "integrity": "sha1-hF/wwINKPe2dFg2sptOQkGuyiMw=", + "dev": true, + "requires": { + "lru-cache": "^4.0.0", + "which": "^1.2.8" + } + }, "crypt": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", @@ -12988,6 +13073,31 @@ "integrity": "sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw==", "dev": true }, + "mount-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mount-point/-/mount-point-3.0.0.tgz", + "integrity": "sha1-Zly57evoDREOZY21bDHQrvUaj5c=", + "dev": true, + "requires": { + "@sindresorhus/df": "^1.0.1", + "pify": "^2.3.0", + "pinkie-promise": "^2.0.1" + }, + "dependencies": { + "@sindresorhus/df": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/df/-/df-1.0.1.tgz", + "integrity": "sha1-xptm9S9vzdKHyAffIQMF2694UA0=", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -13002,6 +13112,34 @@ "run-queue": "^1.0.3" } }, + "move-file": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/move-file/-/move-file-1.2.0.tgz", + "integrity": "sha512-USHrRmxzGowUWAGBbJPdFjHzEqtxDU03pLHY0Rfqgtnq+q8FOIs8wvkkf+Udmg77SJKs47y9sI0jJvQeYsmiCA==", + "dev": true, + "requires": { + "cp-file": "^6.1.0", + "make-dir": "^3.0.0", + "path-exists": "^3.0.0" + }, + "dependencies": { + "make-dir": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", + "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -13160,6 +13298,12 @@ "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", "dev": true }, + "nested-error-stacks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz", + "integrity": "sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==", + "dev": true + }, "next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", @@ -13241,6 +13385,11 @@ "is-stream": "^1.0.1" } }, + "node-gyp-build": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.1.tgz", + "integrity": "sha512-XyCKXsqZfLqHep1hhsMncoXuUNt/cXCjg1+8CLbu69V1TKuPiOeSGbL9n+k/ByKH8UT0p4rdIX8XkTRZV0i7Sw==" + }, "node-has-native-dependencies": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/node-has-native-dependencies/-/node-has-native-dependencies-1.0.2.tgz", @@ -16159,6 +16308,32 @@ "through2": "^2.0.3" } }, + "remove-files-webpack-plugin": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/remove-files-webpack-plugin/-/remove-files-webpack-plugin-1.4.0.tgz", + "integrity": "sha512-qlHn4EHNjWi6LiNV6aWCXROKjQg28sF/VxJ2FK+p5pPUQyLIBGlBAs/TUMxdVeToHSxq2RuA2XGgxcaDeTRnUg==", + "dev": true, + "requires": { + "@types/webpack": "^4.41.6", + "trash": "^6.1.1" + }, + "dependencies": { + "@types/webpack": { + "version": "4.41.7", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.7.tgz", + "integrity": "sha512-OQG9viYwO0V1NaNV7d0n79V+n6mjOV30CwgFPIfTzwmk8DHbt+C4f2aBGdCYbo3yFyYD6sjXfqqOjwkl1j+ulA==", + "dev": true, + "requires": { + "@types/anymatch": "*", + "@types/node": "*", + "@types/tapable": "*", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "source-map": "^0.6.0" + } + } + } + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -18709,6 +18884,55 @@ "integrity": "sha512-S6L67Z8V3WEyPm2/zDh3I3bO0OQwv88dh7IY2dIOVBfIZJ4WQGdEKOsh7phTgYkvfAmHRxfOPNt1ixN/zR6D/A==", "dev": true }, + "trash": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/trash/-/trash-6.1.1.tgz", + "integrity": "sha512-4i56lCmz2RG6WZN018hf4L75L5HboaFuKkHx3wDG/ihevI99e0OgFyl8w6G4ioqBm62V4EJqCy5xw3vQSNXU8A==", + "dev": true, + "requires": { + "@stroncium/procfs": "^1.0.0", + "globby": "^7.1.1", + "is-path-inside": "^3.0.2", + "make-dir": "^3.0.0", + "move-file": "^1.1.0", + "p-map": "^3.0.0", + "p-try": "^2.2.0", + "uuid": "^3.3.2", + "xdg-trashdir": "^2.1.1" + }, + "dependencies": { + "is-path-inside": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", + "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==", + "dev": true + }, + "make-dir": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", + "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, "tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -19531,6 +19755,15 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, + "user-home": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", + "dev": true, + "requires": { + "os-homedir": "^1.0.0" + } + }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", @@ -21186,6 +21419,36 @@ "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=", "dev": true }, + "xdg-basedir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-2.0.0.tgz", + "integrity": "sha1-7byQPMOF/ARSPZZqM1UEtVBNG9I=", + "dev": true, + "requires": { + "os-homedir": "^1.0.0" + } + }, + "xdg-trashdir": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/xdg-trashdir/-/xdg-trashdir-2.1.1.tgz", + "integrity": "sha512-KcVhPaOu2ZurYNHSRTf1+ZHORkTZGCQ+u0JHN17QixRISJq4pXOnjt/lQcehvtHL5QAKhSzKgyjrcNnPdkPBHA==", + "dev": true, + "requires": { + "@sindresorhus/df": "^2.1.0", + "mount-point": "^3.0.0", + "pify": "^2.2.0", + "user-home": "^2.0.0", + "xdg-basedir": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, "xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", @@ -21411,6 +21674,14 @@ "integrity": "sha512-kKfnnYkbTfrAdd0xICNFw7Atm8nKpLcLv9AZGEt+kczL/WQVai4e2V6ZN8U/O+iI6WrNuJjNNOyu4zfhl9D3Hg==", "dev": true }, + "zeromq": { + "version": "6.0.0-beta.6", + "resolved": "https://registry.npmjs.org/zeromq/-/zeromq-6.0.0-beta.6.tgz", + "integrity": "sha512-wLf6M7pBHijl+BRltUL2VoDpgbQcOZetiX8UzycHL8CcYFxYnRrpoG5fi3UX3+Umavz1lk4/dGaQez8qiDgr/Q==", + "requires": { + "node-gyp-build": "^4.1.0" + } + }, "zip-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.0.1.tgz", diff --git a/package.json b/package.json index 7dcbb4a6f84e..5f9c408a01d8 100644 --- a/package.json +++ b/package.json @@ -2954,7 +2954,8 @@ "winreg": "^1.2.4", "winston": "^3.2.1", "ws": "^6.0.0", - "xml2js": "^0.4.19" + "xml2js": "^0.4.19", + "zeromq": "^6.0.0-beta.6" }, "devDependencies": { "@babel/cli": "^7.4.4", @@ -3109,6 +3110,7 @@ "redux": "^4.0.4", "redux-logger": "^3.0.6", "relative": "^3.0.2", + "remove-files-webpack-plugin": "^1.4.0", "rewiremock": "^3.13.0", "sass-loader": "^7.1.0", "serialize-javascript": "^2.1.2", diff --git a/package.nls.json b/package.nls.json index 658700bd7019..8a0db0d40533 100644 --- a/package.nls.json +++ b/package.nls.json @@ -450,5 +450,10 @@ "DataScience.createdNewNotebook": "{0}: Creating new notebook ", "DataScience.createdNewKernel": "{0}: Kernel started: {1}", "DataScience.kernelInvalid": "Kernel {0} is not usable. Check the Jupyter output tab for more information.", - "OutdatedDebugger.updateDebuggerMessage": "We noticed you are attaching to ptvsd (Python debugger), which will be deprecated on May 1st, 2020. Please switch to [debugpy](https://aka.ms/migrateToDebugpy)." + "OutdatedDebugger.updateDebuggerMessage": "We noticed you are attaching to ptvsd (Python debugger), which will be deprecated on May 1st, 2020. Please switch to [debugpy](https://aka.ms/migrateToDebugpy).", + "DataScience.nativeDependencyFail" : "We cannot launch a jupyter server because your OS is not supported. Select an already running server if you wish to continue. {0}", + "DataScience.selectNewServer": "Pick Running Server", + "DataScience.jupyterSelectURIRemoteLabel":"Existing", + "DataScience.jupyterSelectURIQuickPickTitleRemoteOnly": "Pick an already running jupyter server", + "DataScience.jupyterSelectURIRemoteDetail": "Specify the URI of an existing server" } diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 67dc5bb043e0..a6c017df4d00 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -808,6 +808,22 @@ export namespace DataScience { 'DataScience.kernelInvalid', 'Kernel {0} is not usable. Check the Jupyter output tab for more information.' ); + + export const nativeDependencyFail = localize( + 'DataScience.nativeDependencyFail', + '{0}. We cannot launch a jupyter server for you because your OS is not supported. Select an already running server if you wish to continue.' + ); + + export const selectNewServer = localize('DataScience.selectNewServer', 'Pick Running Server'); + export const jupyterSelectURIRemoteLabel = localize('DataScience.jupyterSelectURIRemoteLabel', 'Existing'); + export const jupyterSelectURIQuickPickTitleRemoteOnly = localize( + 'DataScience.jupyterSelectURIQuickPickTitleRemoteOnly', + 'Pick an already running jupyter server' + ); + export const jupyterSelectURIRemoteDetail = localize( + 'DataScience.jupyterSelectURIRemoteDetail', + 'Specify the URI of an existing server' + ); } export namespace DebugConfigStrings { diff --git a/src/client/datascience/commands/serverSelector.ts b/src/client/datascience/commands/serverSelector.ts index 361da4659fa2..ca0a305d4fa1 100644 --- a/src/client/datascience/commands/serverSelector.ts +++ b/src/client/datascience/commands/serverSelector.ts @@ -20,7 +20,7 @@ export class JupyterServerSelectorCommand implements IDisposable { this.disposables.push( this.commandManager.registerCommand( Commands.SelectJupyterURI, - this.serverSelector.selectJupyterURI, + () => this.serverSelector.selectJupyterURI(true), this.serverSelector ) ); diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts index a4a2b91f64d0..02cb95a1c6a9 100644 --- a/src/client/datascience/constants.ts +++ b/src/client/datascience/constants.ts @@ -283,7 +283,8 @@ export enum Telemetry { JupyterCommandLineNonDefault = 'DS_INTERNAL.JUPYTER_CUSTOM_COMMAND_LINE', NewFileForInteractiveWindow = 'DS_INTERNAL.NEW_FILE_USED_IN_INTERACTIVE', KernelInvalid = 'DS_INTERNAL.INVALID_KERNEL_USED', - GatherCompleted = 'DATASCIENCE.GATHER_COMPLETED' + GatherCompleted = 'DATASCIENCE.GATHER_COMPLETED', + ZMQNotSupported = 'DATASCIENCE.ZMQ_NATIVE_BINARIES_NOT_LOADING' } export enum NativeKeyboardCommandTelemetry { diff --git a/src/client/datascience/errorHandler/errorHandler.ts b/src/client/datascience/errorHandler/errorHandler.ts index 658f9c837211..6f5245e79782 100644 --- a/src/client/datascience/errorHandler/errorHandler.ts +++ b/src/client/datascience/errorHandler/errorHandler.ts @@ -3,21 +3,26 @@ import { inject, injectable } from 'inversify'; import { IApplicationShell } from '../../common/application/types'; import { traceError } from '../../common/logger'; +import * as localize from '../../common/utils/localize'; import { noop } from '../../common/utils/misc'; import { JupyterInstallError } from '../jupyter/jupyterInstallError'; import { JupyterSelfCertsError } from '../jupyter/jupyterSelfCertsError'; +import { JupyterZMQBinariesNotFoundError } from '../jupyter/jupyterZMQBinariesNotFoundError'; +import { JupyterServerSelector } from '../jupyter/serverSelector'; import { IDataScienceErrorHandler, IJupyterInterpreterDependencyManager } from '../types'; - @injectable() export class DataScienceErrorHandler implements IDataScienceErrorHandler { constructor( @inject(IApplicationShell) private applicationShell: IApplicationShell, - @inject(IJupyterInterpreterDependencyManager) protected dependencyManager: IJupyterInterpreterDependencyManager + @inject(IJupyterInterpreterDependencyManager) protected dependencyManager: IJupyterInterpreterDependencyManager, + @inject(JupyterServerSelector) private serverSelector: JupyterServerSelector ) {} public async handleError(err: Error): Promise { if (err instanceof JupyterInstallError) { await this.dependencyManager.installMissingDependencies(err); + } else if (err instanceof JupyterZMQBinariesNotFoundError) { + await this.showZMQError(err); } else if (err instanceof JupyterSelfCertsError) { // Don't show the message for self cert errors noop(); @@ -28,4 +33,16 @@ export class DataScienceErrorHandler implements IDataScienceErrorHandler { } traceError('DataScience Error', err); } + + private async showZMQError(err: JupyterZMQBinariesNotFoundError) { + // Ask the user to always pick remote as this is their only option + const selectNewServer = localize.DataScience.selectNewServer(); + this.applicationShell + .showErrorMessage(localize.DataScience.nativeDependencyFail().format(err.toString()), selectNewServer) + .then(selection => { + if (selection === selectNewServer) { + this.serverSelector.selectJupyterURI(false).ignoreErrors(); + } + }); + } } diff --git a/src/client/datascience/interactive-common/interactiveBase.ts b/src/client/datascience/interactive-common/interactiveBase.ts index cadd8538381f..9ae01080b7cc 100644 --- a/src/client/datascience/interactive-common/interactiveBase.ts +++ b/src/client/datascience/interactive-common/interactiveBase.ts @@ -3,6 +3,7 @@ 'use strict'; import '../../common/extensions'; +import { nbformat } from '@jupyterlab/coreutils'; import { injectable, unmanaged } from 'inversify'; import * as os from 'os'; import * as path from 'path'; @@ -22,8 +23,6 @@ import { ViewColumn } from 'vscode'; import { Disposable } from 'vscode-jsonrpc'; - -import { nbformat } from '@jupyterlab/coreutils'; import { ServerStatus } from '../../../datascience-ui/interactive-common/mainState'; import { IApplicationShell, @@ -65,6 +64,7 @@ import { import { JupyterInstallError } from '../jupyter/jupyterInstallError'; import { JupyterInvalidKernelError } from '../jupyter/jupyterInvalidKernelError'; import { JupyterSelfCertsError } from '../jupyter/jupyterSelfCertsError'; +import { JupyterZMQBinariesNotFoundError } from '../jupyter/jupyterZMQBinariesNotFoundError'; import { JupyterKernelPromiseFailedError } from '../jupyter/kernels/jupyterKernelPromiseFailedError'; import { KernelSwitcher } from '../jupyter/kernels/kernelSwitcher'; import { LiveKernelModel } from '../jupyter/kernels/types'; @@ -839,7 +839,7 @@ export abstract class InteractiveBase extends WebViewHost = new EventEmitter(); private disposed: boolean = false; private readonly jupyterInterpreterService: IJupyterSubCommandExecutionService; + private zmqError: Error | undefined; constructor( _liveShare: ILiveShareApi, @@ -330,12 +334,37 @@ export class JupyterExecutionBase implements IJupyterExecution { return Promise.resolve(undefined); } + private async verifyZMQ() { + if (this.zmqError) { + throw this.zmqError; + } + try { + const zmq = await import('zeromq'); + const sock = new zmq.Push(); + const port = await portfinder.getPortPromise(); + + await sock.bind(`tcp://127.0.0.1:${port}`); + sock.send('some work').ignoreErrors(); // This will never return unless there's a listener. Just used for testing the API is available + await sleep(50); + sock.close(); + traceInfo(`ZMQ connection to port ${port} verified.`); + } catch (e) { + traceError(`Exception while attempting zmq :`, e); + sendTelemetryEvent(Telemetry.ZMQNotSupported); + this.zmqError = new JupyterZMQBinariesNotFoundError(e.toString()); + throw this.zmqError; + } + } private async startOrConnect( options?: INotebookServerOptions, cancelToken?: CancellationToken ): Promise { // If our uri is undefined or if it's set to local launch we need to launch a server locally if (!options || !options.uri) { + // First verify we have ZMQ installed correctly (this might change when we don't 'launch' servers anymore) + await this.verifyZMQ(); + + // If that works, then attempt to start the server traceInfo(`Launching ${options ? options.purpose : 'unknown type of'} server`); const useDefaultConfig = options && options.useDefaultConfig ? true : false; const connection = await this.startNotebookServer( diff --git a/src/client/datascience/jupyter/jupyterZMQBinariesNotFoundError.ts b/src/client/datascience/jupyter/jupyterZMQBinariesNotFoundError.ts new file mode 100644 index 000000000000..96a5e47107aa --- /dev/null +++ b/src/client/datascience/jupyter/jupyterZMQBinariesNotFoundError.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +export class JupyterZMQBinariesNotFoundError extends Error { + constructor(message: string) { + super(message); + } +} diff --git a/src/client/datascience/jupyter/serverSelector.ts b/src/client/datascience/jupyter/serverSelector.ts index bfade9c79cd5..4478c0c41aef 100644 --- a/src/client/datascience/jupyter/serverSelector.ts +++ b/src/client/datascience/jupyter/serverSelector.ts @@ -29,6 +29,7 @@ interface ISelectUriQuickPickItem extends QuickPickItem { export class JupyterServerSelector { private readonly localLabel = `$(zap) ${DataScience.jupyterSelectURILocalLabel()}`; private readonly newLabel = `$(server) ${DataScience.jupyterSelectURINewLabel()}`; + private readonly remoteLabel = `$(server) ${DataScience.jupyterSelectURIRemoteLabel()}`; constructor( @inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, @inject(IClipboard) private readonly clipboard: IClipboard, @@ -38,18 +39,24 @@ export class JupyterServerSelector { ) {} @captureTelemetry(Telemetry.SelectJupyterURI) - public selectJupyterURI(): Promise { + public selectJupyterURI(allowLocal: boolean): Promise { const multiStep = this.multiStepFactory.create<{}>(); - return multiStep.run(this.startSelectingURI.bind(this), {}); + return multiStep.run(this.startSelectingURI.bind(this, allowLocal), {}); } - private async startSelectingURI(input: IMultiStepInput<{}>, _state: {}): Promise | void> { + private async startSelectingURI( + allowLocal: boolean, + input: IMultiStepInput<{}>, + _state: {} + ): Promise | void> { // First step, show a quick pick to choose either the remote or the local. // newChoice element will be set if the user picked 'enter a new server' const item = await input.showQuickPick>({ placeholder: DataScience.jupyterSelectURIQuickPickPlaceholder(), - items: this.getUriPickList(), - title: DataScience.jupyterSelectURIQuickPickTitle() + items: this.getUriPickList(allowLocal), + title: allowLocal + ? DataScience.jupyterSelectURIQuickPickTitle() + : DataScience.jupyterSelectURIQuickPickTitleRemoteOnly() }); if (item.label === this.localLabel) { await this.setJupyterURIToLocal(); @@ -131,11 +138,19 @@ export class JupyterServerSelector { } }; - private getUriPickList(): ISelectUriQuickPickItem[] { + private getUriPickList(allowLocal: boolean): ISelectUriQuickPickItem[] { // Always have 'local' and 'add new' const items: ISelectUriQuickPickItem[] = []; - items.push({ label: this.localLabel, detail: DataScience.jupyterSelectURILocalDetail(), newChoice: false }); - items.push({ label: this.newLabel, detail: DataScience.jupyterSelectURINewDetail(), newChoice: true }); + if (allowLocal) { + items.push({ label: this.localLabel, detail: DataScience.jupyterSelectURILocalDetail(), newChoice: false }); + items.push({ label: this.newLabel, detail: DataScience.jupyterSelectURINewDetail(), newChoice: true }); + } else { + items.push({ + label: this.remoteLabel, + detail: DataScience.jupyterSelectURIRemoteDetail(), + newChoice: true + }); + } // Get our list of recent server connections and display that as well const savedURIList = getSavedUriList(this.globalState); diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 33fcc0d74ac7..aba19912ec31 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1896,4 +1896,8 @@ export interface IEventNamePropertyMapping { */ result: 'err' | 'script' | 'notebook'; }; + /** + * Telemetry event sent when the ZMQ native binaries do not work. + */ + [Telemetry.ZMQNotSupported]: undefined | never; } diff --git a/src/test/datascience/commands/serverSelector.unit.test.ts b/src/test/datascience/commands/serverSelector.unit.test.ts index b32816421cf5..cf9ff72fb349 100644 --- a/src/test/datascience/commands/serverSelector.unit.test.ts +++ b/src/test/datascience/commands/serverSelector.unit.test.ts @@ -35,6 +35,6 @@ suite('Data Science - Server Selector Command', () => { handler(); - verify(serverSelector.selectJupyterURI()).once(); + verify(serverSelector.selectJupyterURI(true)).once(); }); }); diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index d8217add53f5..1b752031e85f 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -209,6 +209,7 @@ import { KernelService } from '../../client/datascience/jupyter/kernels/kernelSe import { KernelSwitcher } from '../../client/datascience/jupyter/kernels/kernelSwitcher'; import { NotebookStarter } from '../../client/datascience/jupyter/notebookStarter'; import { ServerPreload } from '../../client/datascience/jupyter/serverPreload'; +import { JupyterServerSelector } from '../../client/datascience/jupyter/serverSelector'; import { PlotViewer } from '../../client/datascience/plotting/plotViewer'; import { PlotViewerProvider } from '../../client/datascience/plotting/plotViewerProvider'; import { ProgressReporter } from '../../client/datascience/progress/progressReporter'; @@ -581,6 +582,12 @@ export class DataScienceIocContainer extends UnitTestIocContainer { mockExtensionContext.setup(m => m.globalStoragePath).returns(() => os.tmpdir()); this.serviceManager.addSingletonInstance(IExtensionContext, mockExtensionContext.object); + const mockServerSelector = mock(JupyterServerSelector); + this.serviceManager.addSingletonInstance( + JupyterServerSelector, + instance(mockServerSelector) + ); + this.serviceManager.addSingleton(ITerminalHelper, TerminalHelper); this.serviceManager.addSingleton( ITerminalActivationCommandProvider, diff --git a/src/test/datascience/errorHandler.unit.test.ts b/src/test/datascience/errorHandler.unit.test.ts index 1abadec5226c..e67697a7b2fd 100644 --- a/src/test/datascience/errorHandler.unit.test.ts +++ b/src/test/datascience/errorHandler.unit.test.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. 'use strict'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as typemoq from 'typemoq'; import { IApplicationShell } from '../../client/common/application/types'; import { IInstallationChannelManager, IModuleInstaller } from '../../client/common/installer/types'; @@ -9,18 +10,25 @@ import { DataScienceErrorHandler } from '../../client/datascience/errorHandler/e import { JupyterCommandInterpreterDependencyService } from '../../client/datascience/jupyter/interpreter/jupyterCommandInterpreterDependencyService'; import { JupyterInstallError } from '../../client/datascience/jupyter/jupyterInstallError'; import { JupyterSelfCertsError } from '../../client/datascience/jupyter/jupyterSelfCertsError'; +import { JupyterZMQBinariesNotFoundError } from '../../client/datascience/jupyter/jupyterZMQBinariesNotFoundError'; +import { JupyterServerSelector } from '../../client/datascience/jupyter/serverSelector'; suite('DataScience Error Handler Unit Tests', () => { let applicationShell: typemoq.IMock; let channels: typemoq.IMock; let dependencyManager: JupyterCommandInterpreterDependencyService; let dataScienceErrorHandler: DataScienceErrorHandler; + const serverSelector = mock(JupyterServerSelector); setup(() => { applicationShell = typemoq.Mock.ofType(); channels = typemoq.Mock.ofType(); dependencyManager = new JupyterCommandInterpreterDependencyService(applicationShell.object, channels.object); - dataScienceErrorHandler = new DataScienceErrorHandler(applicationShell.object, dependencyManager); + dataScienceErrorHandler = new DataScienceErrorHandler( + applicationShell.object, + dependencyManager, + instance(serverSelector) + ); }); const message = 'Test error message.'; @@ -89,4 +97,18 @@ suite('DataScience Error Handler Unit Tests', () => { applicationShell.verifyAll(); channels.verifyAll(); }); + + test('ZMQ Install Error', async () => { + applicationShell + .setup(app => + app.showErrorMessage(typemoq.It.isAny(), typemoq.It.isValue(localize.DataScience.selectNewServer())) + ) + .returns(() => Promise.resolve(localize.DataScience.selectNewServer())) + .verifiable(typemoq.Times.once()); + when(serverSelector.selectJupyterURI(anything())).thenCall(() => Promise.resolve()); + const err = new JupyterZMQBinariesNotFoundError('Not found'); + await dataScienceErrorHandler.handleError(err); + verify(serverSelector.selectJupyterURI(anything())).once(); + applicationShell.verifyAll(); + }); }); diff --git a/src/test/datascience/jupyter/serverSelector.unit.test.ts b/src/test/datascience/jupyter/serverSelector.unit.test.ts index 526700658842..f8cbab2ecbd2 100644 --- a/src/test/datascience/jupyter/serverSelector.unit.test.ts +++ b/src/test/datascience/jupyter/serverSelector.unit.test.ts @@ -72,11 +72,11 @@ suite('Data Science - Jupyter Server URI Selector', () => { test('Local pick server uri', async () => { let value = ''; const ds = createDataScienceObject('$(zap) Default', '', v => (value = v)); - await ds.selectJupyterURI(); + await ds.selectJupyterURI(true); assert.equal(value, Settings.JupyterServerLocalLaunch, 'Default should pick local launch'); // Try a second time. - await ds.selectJupyterURI(); + await ds.selectJupyterURI(true); assert.equal(value, Settings.JupyterServerLocalLaunch, 'Default should pick local launch'); // Verify active items @@ -94,7 +94,7 @@ suite('Data Science - Jupyter Server URI Selector', () => { mockStorage ); - await ds.selectJupyterURI(); + await ds.selectJupyterURI(true); // Verify initial default items assert.equal(quickPick?.items.length, 2, 'Wrong number of items in the quick pick'); @@ -102,14 +102,14 @@ suite('Data Science - Jupyter Server URI Selector', () => { const serverA1 = { uri: 'ServerA', time: 1, date: new Date(1) }; addToUriList(mockStorage, serverA1.uri, serverA1.time); - await ds.selectJupyterURI(); + await ds.selectJupyterURI(true); assert.equal(quickPick?.items.length, 3, 'Wrong number of items in the quick pick'); quickPickCheck(quickPick?.items[2], serverA1); // Add in a second server, the newer server should be higher in the list due to newer time const serverB1 = { uri: 'ServerB', time: 2, date: new Date(2) }; addToUriList(mockStorage, serverB1.uri, serverB1.time); - await ds.selectJupyterURI(); + await ds.selectJupyterURI(true); assert.equal(quickPick?.items.length, 4, 'Wrong number of items in the quick pick'); quickPickCheck(quickPick?.items[2], serverB1); quickPickCheck(quickPick?.items[3], serverA1); @@ -117,7 +117,7 @@ suite('Data Science - Jupyter Server URI Selector', () => { // Reconnect to server A with a new time, it should now be higher in the list const serverA3 = { uri: 'ServerA', time: 3, date: new Date(3) }; addToUriList(mockStorage, serverA3.uri, serverA3.time); - await ds.selectJupyterURI(); + await ds.selectJupyterURI(true); assert.equal(quickPick?.items.length, 4, 'Wrong number of items in the quick pick'); quickPickCheck(quickPick?.items[3], serverB1); quickPickCheck(quickPick?.items[2], serverA1); @@ -127,7 +127,7 @@ suite('Data Science - Jupyter Server URI Selector', () => { addToUriList(mockStorage, i.toString(), i); } - await ds.selectJupyterURI(); + await ds.selectJupyterURI(true); // Need a plus 2 here for the two default items assert.equal( quickPick?.items.length, @@ -151,14 +151,21 @@ suite('Data Science - Jupyter Server URI Selector', () => { test('Remote server uri', async () => { let value = ''; const ds = createDataScienceObject('$(server) Existing', 'http://localhost:1111', v => (value = v)); - await ds.selectJupyterURI(); + await ds.selectJupyterURI(true); + assert.equal(value, 'http://localhost:1111', 'Already running should end up with the user inputed value'); + }); + + test('Remote server uri no local', async () => { + let value = ''; + const ds = createDataScienceObject('$(server) Existing', 'http://localhost:1111', v => (value = v)); + await ds.selectJupyterURI(false); assert.equal(value, 'http://localhost:1111', 'Already running should end up with the user inputed value'); }); test('Remote server uri (reload VSCode if there is a change in settings)', async () => { let value = ''; const ds = createDataScienceObject('$(server) Existing', 'http://localhost:1111', v => (value = v)); - await ds.selectJupyterURI(); + await ds.selectJupyterURI(true); assert.equal(value, 'http://localhost:1111', 'Already running should end up with the user inputed value'); verify(cmdManager.executeCommand(anything(), anything())).once(); }); @@ -167,7 +174,7 @@ suite('Data Science - Jupyter Server URI Selector', () => { let value = ''; const ds = createDataScienceObject('$(server) Existing', 'http://localhost:1111', v => (value = v)); dsSettings.jupyterServerURI = 'http://localhost:1111'; - await ds.selectJupyterURI(); + await ds.selectJupyterURI(true); assert.equal(value, 'http://localhost:1111', 'Already running should end up with the user inputed value'); verify(cmdManager.executeCommand(anything(), anything())).never(); }); @@ -175,7 +182,7 @@ suite('Data Science - Jupyter Server URI Selector', () => { test('Invalid server uri', async () => { let value = ''; const ds = createDataScienceObject('$(server) Existing', 'httx://localhost:1111', v => (value = v)); - await ds.selectJupyterURI(); + await ds.selectJupyterURI(true); assert.notEqual(value, 'httx://localhost:1111', 'Already running should validate'); assert.equal(value, '', 'Validation failed'); }); @@ -188,7 +195,7 @@ suite('Data Science - Jupyter Server URI Selector', () => { const ds = createDataScienceObject('$(server) Existing', 'http://localhost:1111', noop); when(clipboard.readText()).thenResolve(clipboardValue || ''); - await ds.selectJupyterURI(); + await ds.selectJupyterURI(true); assert.equal(showInputBox.firstCall.args[0].value, expectedDefaultUri); }