diff --git a/package-lock.json b/package-lock.json index 0e56694..e64ef32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "fs-extra": "^9.1.0", "handlebars": "^4.7.7", "ps-list": "^7.2.0", + "semver": "^5.7.1", "vscode-azureextensionui": "^0.40.0", "vscode-nls": "^5.0.0" }, @@ -24,8 +25,9 @@ "@types/mocha": "^8.2.2", "@types/node": "^12.20.1", "@types/ps-list": "^6.2.1", + "@types/semver": "^7.3.6", "@types/terser-webpack-plugin": "^5.0.2", - "@types/vscode": "^1.54.0", + "@types/vscode": "^1.57.0", "@types/webpack": "^4.41.26", "@types/which": "^2.0.1", "@typescript-eslint/eslint-plugin": "^4.19.0", @@ -49,7 +51,9 @@ "webpack": "^5.28.0" }, "engines": { - "vscode": "^1.54.0" + "dapr-cli": ">=1.0", + "dapr-runtime": ">=1.0", + "vscode": "^1.57.0" } }, "node_modules/@azure/abort-controller": { @@ -607,6 +611,12 @@ "ps-list": "*" } }, + "node_modules/@types/semver": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.6.tgz", + "integrity": "sha512-0caWDWmpCp0uifxFh+FaqK3CuZ2SkRR/ZRxAV5+zNdC3QVUi6wyOJnefhPvtNt8NQWXB5OA93BUvZsXpWat2Xw==", + "dev": true + }, "node_modules/@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", @@ -685,9 +695,9 @@ } }, "node_modules/@types/vscode": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.54.0.tgz", - "integrity": "sha512-sHHw9HG4bTrnKhLGgmEiOS88OLO/2RQytUN4COX9Djv81zc0FSZsSiYaVyjNidDzUSpXsySKBkZ31lk2/FbdCg==", + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.57.0.tgz", + "integrity": "sha512-FeznBFtIDCWRluojTsi9c3LLcCHOXP5etQfBK42+ixo1CoEAchkw39tuui9zomjZuKfUVL33KZUDIwHZ/xvOkQ==", "dev": true }, "node_modules/@types/webpack": { @@ -1719,16 +1729,16 @@ "dev": true }, "node_modules/browserslist": { - "version": "4.16.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.3.tgz", - "integrity": "sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==", + "version": "4.16.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", + "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", "dev": true, "dependencies": { - "caniuse-lite": "^1.0.30001181", - "colorette": "^1.2.1", - "electron-to-chromium": "^1.3.649", + "caniuse-lite": "^1.0.30001219", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.723", "escalade": "^3.1.1", - "node-releases": "^1.1.70" + "node-releases": "^1.1.71" }, "bin": { "browserslist": "cli.js" @@ -1737,6 +1747,18 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/browserslist/node_modules/caniuse-lite": { + "version": "1.0.30001230", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001230.tgz", + "integrity": "sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ==", + "dev": true + }, + "node_modules/browserslist/node_modules/electron-to-chromium": { + "version": "1.3.738", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.738.tgz", + "integrity": "sha512-vCMf4gDOpEylPSLPLSwAEsz+R3ShP02Y3cAKMZvTqule3XcPp7tgc/0ESI7IS6ZeyBlGClE50N53fIOkcIVnpw==", + "dev": true + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -1826,12 +1848,6 @@ "node": ">=6" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001204", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001204.tgz", - "integrity": "sha512-JUdjWpcxfJ9IPamy2f5JaRDCaqJOxDzOSKtbdx4rH9VivMd1vIzoPumsJa9LoMIi4Fx2BV2KZOxWhNkBjaYivQ==", - "dev": true - }, "node_modules/chainsaw": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", @@ -2985,12 +3001,6 @@ "object.defaults": "^1.1.0" } }, - "node_modules/electron-to-chromium": { - "version": "1.3.699", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.699.tgz", - "integrity": "sha512-fjt43CPXdPYwD9ybmKbNeLwZBmCVdLY2J5fGZub7/eMPuiqQznOGNXv/wurnpXIlE7ScHnvG9Zi+H4/i6uMKmw==", - "dev": true - }, "node_modules/emitter-listener": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", @@ -4361,7 +4371,71 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.11.tgz", "integrity": "sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw==", "bundleDependencies": [ - "node-pre-gyp" + "abbrev", + "ansi-regex", + "aproba", + "are-we-there-yet", + "balanced-match", + "brace-expansion", + "chownr", + "code-point-at", + "concat-map", + "console-control-strings", + "core-util-is", + "debug", + "deep-extend", + "delegates", + "detect-libc", + "fs-minipass", + "fs.realpath", + "gauge", + "glob", + "has-unicode", + "iconv-lite", + "ignore-walk", + "inflight", + "inherits", + "is-fullwidth-code-point", + "isarray", + "minimatch", + "minimist", + "minipass", + "minizlib", + "mkdirp", + "ms", + "needle", + "node-pre-gyp", + "nopt", + "npm-bundled", + "npm-normalize-package-bin", + "npm-packlist", + "npmlog", + "number-is-nan", + "object-assign", + "once", + "os-homedir", + "os-tmpdir", + "osenv", + "path-is-absolute", + "process-nextick-args", + "rc", + "readable-stream", + "rimraf", + "safe-buffer", + "safer-buffer", + "sax", + "semver", + "set-blocking", + "signal-exit", + "string-width", + "string_decoder", + "strip-ansi", + "strip-json-comments", + "tar", + "util-deprecate", + "wide-align", + "wrappy", + "yallist" ], "dev": true, "optional": true, @@ -11568,6 +11642,12 @@ "ps-list": "*" } }, + "@types/semver": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.6.tgz", + "integrity": "sha512-0caWDWmpCp0uifxFh+FaqK3CuZ2SkRR/ZRxAV5+zNdC3QVUi6wyOJnefhPvtNt8NQWXB5OA93BUvZsXpWat2Xw==", + "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", @@ -11646,9 +11726,9 @@ } }, "@types/vscode": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.54.0.tgz", - "integrity": "sha512-sHHw9HG4bTrnKhLGgmEiOS88OLO/2RQytUN4COX9Djv81zc0FSZsSiYaVyjNidDzUSpXsySKBkZ31lk2/FbdCg==", + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.57.0.tgz", + "integrity": "sha512-FeznBFtIDCWRluojTsi9c3LLcCHOXP5etQfBK42+ixo1CoEAchkw39tuui9zomjZuKfUVL33KZUDIwHZ/xvOkQ==", "dev": true }, "@types/webpack": { @@ -12516,16 +12596,30 @@ "dev": true }, "browserslist": { - "version": "4.16.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.3.tgz", - "integrity": "sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==", + "version": "4.16.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", + "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001181", - "colorette": "^1.2.1", - "electron-to-chromium": "^1.3.649", + "caniuse-lite": "^1.0.30001219", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.723", "escalade": "^3.1.1", - "node-releases": "^1.1.70" + "node-releases": "^1.1.71" + }, + "dependencies": { + "caniuse-lite": { + "version": "1.0.30001230", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001230.tgz", + "integrity": "sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.738", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.738.tgz", + "integrity": "sha512-vCMf4gDOpEylPSLPLSwAEsz+R3ShP02Y3cAKMZvTqule3XcPp7tgc/0ESI7IS6ZeyBlGClE50N53fIOkcIVnpw==", + "dev": true + } } }, "buffer-crc32": { @@ -12593,12 +12687,6 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, - "caniuse-lite": { - "version": "1.0.30001204", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001204.tgz", - "integrity": "sha512-JUdjWpcxfJ9IPamy2f5JaRDCaqJOxDzOSKtbdx4rH9VivMd1vIzoPumsJa9LoMIi4Fx2BV2KZOxWhNkBjaYivQ==", - "dev": true - }, "chainsaw": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", @@ -13644,12 +13732,6 @@ "object.defaults": "^1.1.0" } }, - "electron-to-chromium": { - "version": "1.3.699", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.699.tgz", - "integrity": "sha512-fjt43CPXdPYwD9ybmKbNeLwZBmCVdLY2J5fGZub7/eMPuiqQznOGNXv/wurnpXIlE7ScHnvG9Zi+H4/i6uMKmw==", - "dev": true - }, "emitter-listener": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", diff --git a/package.json b/package.json index f12a21c..3e684af 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "ui" ], "engines": { - "vscode": "^1.54.0" + "dapr-cli": ">=1.0", + "dapr-runtime": ">=1.0", + "vscode": "^1.57.0" }, "categories": [ "Debuggers", @@ -33,6 +35,7 @@ "onCommand:vscode-dapr.applications.publish-message", "onCommand:vscode-dapr.applications.stop-app", "onCommand:vscode-dapr.help.getStarted", + "onCommand:vscode-dapr.help.installDapr", "onCommand:vscode-dapr.help.readDocumentation", "onCommand:vscode-dapr.help.reportIssue", "onCommand:vscode-dapr.help.reviewIssues", @@ -80,6 +83,11 @@ "title": "%vscode-dapr.help.getStarted.title%", "category": "Dapr" }, + { + "command": "vscode-dapr.help.installDapr", + "title": "%vscode-dapr.help.installDapr.title%", + "category": "Dapr" + }, { "command": "vscode-dapr.help.readDocumentation", "title": "%vscode-dapr.help.readDocumentation.title%", @@ -435,7 +443,24 @@ "icon": "assets/images/dapr.svg" } ] - } + }, + "viewsWelcome": [ + { + "view": "vscode-dapr.views.applications", + "contents": "%vscode-dapr.views.applications.contents.notInitialized%", + "when": "vscode-dapr.views.applications.state == 'notInitialized'" + }, + { + "view": "vscode-dapr.views.applications", + "contents": "%vscode-dapr.views.applications.contents.notInstalled%", + "when": "vscode-dapr.views.applications.state == 'notInstalled'" + }, + { + "view": "vscode-dapr.views.applications", + "contents": "%vscode-dapr.views.applications.contents.notRunning%", + "when": "vscode-dapr.views.applications.state == 'notRunning'" + } + ] }, "scripts": { "clean": "gulp clean", @@ -457,8 +482,9 @@ "@types/mocha": "^8.2.2", "@types/node": "^12.20.1", "@types/ps-list": "^6.2.1", + "@types/semver": "^7.3.6", "@types/terser-webpack-plugin": "^5.0.2", - "@types/vscode": "^1.54.0", + "@types/vscode": "^1.57.0", "@types/webpack": "^4.41.26", "@types/which": "^2.0.1", "@typescript-eslint/eslint-plugin": "^4.19.0", @@ -486,6 +512,7 @@ "fs-extra": "^9.1.0", "handlebars": "^4.7.7", "ps-list": "^7.2.0", + "semver": "^5.7.1", "vscode-azureextensionui": "^0.40.0", "vscode-nls": "^5.0.0" } diff --git a/package.nls.json b/package.nls.json index 4027176..5001187 100644 --- a/package.nls.json +++ b/package.nls.json @@ -10,11 +10,16 @@ "vscode-dapr.help.readDocumentation.title": "Read Documentation", "vscode-dapr.help.getStarted.title": "Get Started", + "vscode-dapr.help.installDapr.title": "Install Dapr", "vscode-dapr.help.reportIssue.title": "Report Issue", "vscode-dapr.help.reviewIssues.title": "Review Issues", "vscode-dapr.tasks.scaffoldDaprComponents.title": "Scaffold Dapr Components", "vscode-dapr.tasks.scaffoldDaprTasks.title": "Scaffold Dapr Tasks", + "vscode-dapr.views.applications.name": "Applications", + "vscode-dapr.views.applications.contents.notInitialized": "A compatible version of the Dapr runtime has not been found. You may need to install a more recent version.\n[Install Latest Dapr](command:vscode-dapr.help.installDapr)", + "vscode-dapr.views.applications.contents.notInstalled": "A compatible version of the Dapr CLI has not been found. You may need to install a more recent version.\n[Install Latest Dapr](command:vscode-dapr.help.installDapr)", + "vscode-dapr.views.applications.contents.notRunning": "No Dapr applications are running.", "vscode-dapr.views.help.name": "Help and Feedback", "vscode-dapr.view-containers.dapr-explorer.title": "Dapr", diff --git a/src/commands/help/installDapr.ts b/src/commands/help/installDapr.ts new file mode 100644 index 0000000..fc97af5 --- /dev/null +++ b/src/commands/help/installDapr.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { UserInput } from '../../services/userInput'; + +export function installDapr(ui: UserInput): Thenable { + return ui.openExternal('https://aka.ms/vscode-dapr-install-dapr'); +} + +const createInstallDaprCommand = (ui: UserInput) => (): Thenable => installDapr(ui); + +export default createInstallDaprCommand; diff --git a/src/extension.ts b/src/extension.ts index 7fd9bb5..a8c1017 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -31,6 +31,12 @@ import NodeEnvironmentProvider from './services/environmentProvider'; import createScaffoldDaprComponentsCommand from './commands/scaffoldDaprComponents'; import VsCodeSettingsProvider from './services/settingsProvider'; import createStopCommand from './commands/applications/stopApp'; +import LocalDaprCliClient from './services/daprCliClient'; +import createInstallDaprCommand from './commands/help/installDapr'; + +interface ExtensionPackage { + engines: { [key: string]: string }; +} export function activate(context: vscode.ExtensionContext): Promise { function registerDisposable(disposable: T): T { @@ -68,21 +74,28 @@ export function activate(context: vscode.ExtensionContext): Promise { telemetryProvider.registerContextCommandWithTelemetry('vscode-dapr.applications.stop-app', createStopCommand(daprClient, ui)); telemetryProvider.registerContextCommandWithTelemetry('vscode-dapr.help.readDocumentation', createReadDocumentationCommand(ui)); telemetryProvider.registerContextCommandWithTelemetry('vscode-dapr.help.getStarted', createGetStartedCommand(ui)); + telemetryProvider.registerContextCommandWithTelemetry('vscode-dapr.help.installDapr', createInstallDaprCommand(ui)); telemetryProvider.registerContextCommandWithTelemetry('vscode-dapr.help.reportIssue', createReportIssueCommand(ui)); telemetryProvider.registerContextCommandWithTelemetry('vscode-dapr.help.reviewIssues', createReviewIssuesCommand(ui)); telemetryProvider.registerCommandWithTelemetry('vscode-dapr.tasks.scaffoldDaprComponents', createScaffoldDaprComponentsCommand(scaffolder, templateScaffolder)); telemetryProvider.registerCommandWithTelemetry('vscode-dapr.tasks.scaffoldDaprTasks', createScaffoldDaprTasksCommand(scaffolder, templateScaffolder, ui)); const settingsProvider = new VsCodeSettingsProvider(); + const extensionPackage = context.extension.packageJSON; + const daprInstallationManager = new LocalDaprInstallationManager( + extensionPackage.engines['dapr-cli'], + extensionPackage.engines['dapr-runtime'], + new LocalDaprCliClient(() => settingsProvider.daprPath), + ui); - registerDisposable(vscode.tasks.registerTaskProvider('dapr', new DaprCommandTaskProvider(() => settingsProvider.daprPath, telemetryProvider))); - registerDisposable(vscode.tasks.registerTaskProvider('daprd', new DaprdCommandTaskProvider(() => settingsProvider.daprdPath, new NodeEnvironmentProvider(), telemetryProvider))); + registerDisposable(vscode.tasks.registerTaskProvider('dapr', new DaprCommandTaskProvider(daprInstallationManager, () => settingsProvider.daprPath, telemetryProvider))); + registerDisposable(vscode.tasks.registerTaskProvider('daprd', new DaprdCommandTaskProvider(daprInstallationManager, () => settingsProvider.daprdPath, new NodeEnvironmentProvider(), telemetryProvider))); registerDisposable(vscode.tasks.registerTaskProvider('daprd-down', new DaprdDownTaskProvider(daprApplicationProvider, telemetryProvider))); registerDisposable( vscode.window.registerTreeDataProvider( 'vscode-dapr.views.applications', - registerDisposable(new DaprApplicationTreeDataProvider(daprApplicationProvider, new LocalDaprInstallationManager(), daprClient)))); + registerDisposable(new DaprApplicationTreeDataProvider(daprApplicationProvider, daprClient, daprInstallationManager, ui)))); registerDisposable( vscode.window.registerTreeDataProvider( diff --git a/src/services/daprCliClient.ts b/src/services/daprCliClient.ts new file mode 100644 index 0000000..cb229cd --- /dev/null +++ b/src/services/daprCliClient.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import CommandLineBuilder from "../util/commandLineBuilder"; +import { Process } from "../util/process"; +import * as nls from 'vscode-nls'; +import { getLocalizationPathForFile } from '../util/localization'; + +const localize = nls.loadMessageBundle(getLocalizationPathForFile(__filename)); + +export interface DaprVersion { + cli: string | undefined; + runtime: string | undefined; +} + +export interface DaprCliClient { + version(): Promise; +} + +export default class LocalDaprCliClient implements DaprCliClient { + constructor(private readonly daprPathProvider: () => string) { + } + + async version(): Promise { + const daprPath = this.daprPathProvider(); + const command = + CommandLineBuilder + .create(daprPath, '--version') + .build(); + + const result = await Process.exec(command); + + if (result.code !== 0) { + throw new Error(localize('services.daprCliClient.versionFailed', 'Retrieving the dapr CLI version failed: {0}', result.stderr)); + } + + const cliMatch = /^CLI version: (?.+)$/gm.exec(result.stdout); + const runtimeMatch = /^Runtime version: (?.+)$/gm.exec(result.stdout); + + return { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + cli: cliMatch ? cliMatch.groups!['version'] : undefined, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + runtime: runtimeMatch ? runtimeMatch.groups!['version'] : undefined + } + } +} \ No newline at end of file diff --git a/src/services/daprInstallationManager.ts b/src/services/daprInstallationManager.ts index dedd64c..91489bb 100644 --- a/src/services/daprInstallationManager.ts +++ b/src/services/daprInstallationManager.ts @@ -1,98 +1,160 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import * as process from 'process'; -import { AsyncLazy } from '../util/lazy'; -import { Process } from '../util/process'; +import * as semver from 'semver'; +import { DaprCliClient } from './daprCliClient'; +import * as nls from 'vscode-nls'; +import { getLocalizationPathForFile } from '../util/localization'; +import { IErrorHandlingContext } from 'vscode-azureextensionui'; +import { UserInput } from './userInput'; -export interface DaprVersion { - cli: string | undefined; - runtime: string | undefined; -} +const localize = nls.loadMessageBundle(getLocalizationPathForFile(__filename)); export interface DaprInstallationManager { - getVersion(): Promise; + ensureInstalled(context?: IErrorHandlingContext): Promise; + ensureInstalledVersion(cliVersion: string, context?: IErrorHandlingContext): Promise; + + ensureInitialized(context?: IErrorHandlingContext): Promise; + ensureInitializedVersion(cliVersion: string, runtimeVersion: string, context?: IErrorHandlingContext): Promise; + + isInstalled(): Promise; + isVersionInstalled(cliVersion: string): Promise; + isInitialized(): Promise; + isVersionInitialized(cliVersion: string, runtimeVersion: string): Promise; } -function getCliVersion(versionOutput: string): string | undefined { - const result = /^CLI version: (?\d+.\d+.\d+)\s*/gm.exec(versionOutput); - - return result?.groups?.['version']; +function isSemverSatisfied(version: string, range: string): boolean { + return semver.satisfies(version, range, { includePrerelease: true }); } -function getRuntimeVersion(versionOutput: string): string | undefined { - const result = /^Runtime version: (?\d+.\d+.\d+)\s*/gm.exec(versionOutput); +const localCliBuildVersion = 'edge'; - return result?.groups?.['version']; +function isCliSemverSatisfied(version: string | undefined, range: string): boolean { + return version === localCliBuildVersion || (version !== undefined && isSemverSatisfied(version, range)); } -const daprImageName = 'daprio/dapr'; -const daprTaggedImagePrefix = `${daprImageName}:`; - export default class LocalDaprInstallationManager implements DaprInstallationManager { - private readonly version: AsyncLazy; - private readonly initialized: AsyncLazy; - - constructor() { - this.version = new AsyncLazy( - async () => { - const versionResult = await Process.exec('dapr --version'); - - if (versionResult.code === 0) { - return { - cli: getCliVersion(versionResult.stdout), - runtime: getRuntimeVersion(versionResult.stdout) - }; - } + private readonly satisfiedCliVersions = new Set(); + private readonly satisfiedRuntimeVersions = new Set(); + + constructor( + private readonly expectedCliVersion: string, + private readonly expectedRuntimeVersion: string, + private readonly daprCliClient: DaprCliClient, + private readonly ui: UserInput) { + } - return undefined; - }); - - this.initialized = new AsyncLazy( - async () => { - const network = process.env.DAPR_NETWORK || 'bridge'; - const psResult = await Process.exec(`docker ps --filter network=${network} --format "{{.ID}}"`); - - if (psResult.code === 0) { - const containerIds = psResult.stdout.split('\n').filter(id => id.length > 0); - - if (containerIds.length > 0) { - const inspectResult = await Process.exec(`docker inspect ${containerIds.join(' ')} --format "{{.Config.Image}}"`); - - if (inspectResult.code === 0) { - const containerImages = inspectResult.stdout.split('\n'); - - if (containerImages.find(image => image === daprImageName || image.startsWith(daprTaggedImagePrefix))) { - return true; - } - } - } - } + ensureInstalled(context?: IErrorHandlingContext): Promise { + return this.ensureInstalledVersion(this.expectedCliVersion, context); + } + + async ensureInstalledVersion(cliVersion: string, context?: IErrorHandlingContext): Promise { + const isVersionInstalled = await this.isVersionInstalled(cliVersion); + + if (!isVersionInstalled) { + this.setErrorHandlingContext(context); - return undefined; - }); + throw new Error(localize('services.daprInstallationManager.versionNotInstalled', 'A compatible version of the Dapr CLI has not been found. You may need to install a more recent version.')); + } } - async getVersion(): Promise { - try { - return await this.version.getValue(); - } catch { - // No-op errors. + ensureInitialized(context?: IErrorHandlingContext): Promise { + return this.ensureInitializedVersion(this.expectedCliVersion, this.expectedRuntimeVersion, context); + } + + async ensureInitializedVersion(cliVersion: string, runtimeVersion: string, context?: IErrorHandlingContext): Promise { + const isVersionInitialized = await this.isVersionInitialized(cliVersion, runtimeVersion); + + if (!isVersionInitialized) { + this.setErrorHandlingContext(context); + + throw new Error(localize('services.daprInstallationManager.versionNotInitialized', 'A compatible version of Dapr has not been initialized. You may need to install a more recent version.')); } + } - return undefined; + isInstalled(): Promise { + return this.isVersionInstalled(this.expectedCliVersion); } - async isInitialized(): Promise { + async isVersionInstalled(cliVersion: string): Promise { + if (this.satisfiedCliVersions.has(cliVersion)) { + return true; + } + try { - if (await this.initialized.getValue()) { + const version = await this.daprCliClient.version(); + + if (isCliSemverSatisfied(version.cli, cliVersion)) { + this.satisfiedCliVersions.add(cliVersion); + return true; } - } catch { - // No-op errors. } + catch { + // No-op. + } + + return false; + } + + isInitialized(): Promise { + return this.isVersionInitialized(this.expectedCliVersion, this.expectedRuntimeVersion); + } + + async isVersionInitialized(cliVersion: string, runtimeVersion: string): Promise { + if (this.satisfiedCliVersions.has(cliVersion) && + this.satisfiedRuntimeVersions.has(runtimeVersion)) { + return true; + } + + try { + const version = await this.daprCliClient.version(); + + let cliVersionSatisfied = false; + + if (isCliSemverSatisfied(version.cli, cliVersion)) { + this.satisfiedCliVersions.add(cliVersion); + + cliVersionSatisfied = true; + } + + let runtimeVersionSatisfied = false; + if (version.runtime !== undefined + && version.runtime !== 'n/a' + && isSemverSatisfied(version.runtime, runtimeVersion)) { + this.satisfiedRuntimeVersions.add(runtimeVersion); + + runtimeVersionSatisfied = true; + } + + if (cliVersionSatisfied && runtimeVersionSatisfied) { + return true; + } + } + catch { + // No-op. + } + return false; } + + private setErrorHandlingContext(context: IErrorHandlingContext | undefined): void { + if (context) { + context.buttons = [ + { + callback: async () => { + await this.ui.executeCommand('vscode-dapr.help.installDapr') + }, + title: localize('services.daprInstallationManager.installLatestTitle', 'Install Latest Dapr') + } + ]; + + context.suppressReportIssue = true; + } + } } diff --git a/src/services/userInput.ts b/src/services/userInput.ts index 89eeb38..b966d11 100644 --- a/src/services/userInput.ts +++ b/src/services/userInput.ts @@ -16,6 +16,7 @@ export interface WizardStep { } export interface UserInput { + executeCommand(command: string, ...rest: unknown[]): Promise; openExternal(url: string): Promise; showInputBox(options: vscode.InputBoxOptions): Promise; showIssueReporter(): Promise; @@ -54,6 +55,10 @@ export class AggregateUserInput implements UserInput { constructor(private readonly ui: IAzureUserInput) { } + async executeCommand(command: string, ...rest: unknown[]): Promise { + await vscode.commands.executeCommand(command, ...rest); + } + async openExternal(url: string): Promise { return await vscode.env.openExternal(vscode.Uri.parse(url, true)); } diff --git a/src/tasks/daprCommandTaskProvider.ts b/src/tasks/daprCommandTaskProvider.ts index 6bcd3f9..8d04606 100644 --- a/src/tasks/daprCommandTaskProvider.ts +++ b/src/tasks/daprCommandTaskProvider.ts @@ -5,6 +5,8 @@ import CommandLineBuilder from '../util/commandLineBuilder'; import CommandTaskProvider from './commandTaskProvider'; import { TaskDefinition } from './taskDefinition'; import { TelemetryProvider } from '../services/telemetryProvider'; +import { IActionContext } from 'vscode-azureextensionui'; +import { DaprInstallationManager } from '../services/daprInstallationManager'; export interface DaprTaskDefinition extends TaskDefinition { appId?: string; @@ -27,12 +29,14 @@ export interface DaprTaskDefinition extends TaskDefinition { } export default class DaprCommandTaskProvider extends CommandTaskProvider { - constructor(daprPathProvider: () => string, telemetryProvider: TelemetryProvider) { + constructor(daprInstallationManager: DaprInstallationManager, daprPathProvider: () => string, telemetryProvider: TelemetryProvider) { super( (definition, callback) => { return telemetryProvider.callWithTelemetry( 'vscode-dapr.tasks.dapr', - () => { + async (context: IActionContext) => { + await daprInstallationManager.ensureInitialized(context.errorHandling); + const daprDefinition = definition as DaprTaskDefinition; const command = diff --git a/src/tasks/daprdCommandTaskProvider.ts b/src/tasks/daprdCommandTaskProvider.ts index 340b4b5..8ed5900 100644 --- a/src/tasks/daprdCommandTaskProvider.ts +++ b/src/tasks/daprdCommandTaskProvider.ts @@ -8,6 +8,8 @@ import CommandTaskProvider from './commandTaskProvider'; import { TaskDefinition } from './taskDefinition'; import { TelemetryProvider } from '../services/telemetryProvider'; import { EnvironmentProvider } from '../services/environmentProvider'; +import { IActionContext } from 'vscode-azureextensionui'; +import { DaprInstallationManager } from '../services/daprInstallationManager'; type DaprdLogLevel = 'debug' | 'info' | 'warning' | 'error' | 'fatal' | 'panic'; @@ -40,6 +42,7 @@ export interface DaprdTaskDefinition extends TaskDefinition { export default class DaprdCommandTaskProvider extends CommandTaskProvider { constructor( + daprInstallationManager: DaprInstallationManager, daprdPathProvider: () => string, environmentProvider: EnvironmentProvider, telemetryProvider: TelemetryProvider) { @@ -47,7 +50,9 @@ export default class DaprdCommandTaskProvider extends CommandTaskProvider { (definition, callback) => { return telemetryProvider.callWithTelemetry( 'vscode-dapr.tasks.daprd', - () => { + async (context: IActionContext) => { + await daprInstallationManager.ensureInitialized(context.errorHandling); + const daprDefinition = definition as DaprdTaskDefinition; const command = @@ -78,7 +83,7 @@ export default class DaprdCommandTaskProvider extends CommandTaskProvider { .withArgs(daprDefinition.args) .build(); - return callback(command, { cwd: definition.cwd }); + await callback(command, { cwd: definition.cwd }); }); }, /* isBackgroundTask: */ true, diff --git a/src/views/applications/daprApplicationTreeDataProvider.ts b/src/views/applications/daprApplicationTreeDataProvider.ts index 6b6097a..b6ae4cf 100644 --- a/src/views/applications/daprApplicationTreeDataProvider.ts +++ b/src/views/applications/daprApplicationTreeDataProvider.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. @@ -5,8 +6,8 @@ import * as vscode from 'vscode'; import { DaprApplicationProvider } from '../../services/daprApplicationProvider'; import TreeNode from '../treeNode'; import DaprApplicationNode from './daprApplicationNode'; -import NoApplicationsRunningNode from './noApplicationsRunningNode'; import { DaprInstallationManager } from '../../services/daprInstallationManager'; +import { UserInput } from '../../services/userInput'; import { DaprClient } from '../../services/daprClient'; export default class DaprApplicationTreeDataProvider extends vscode.Disposable implements vscode.TreeDataProvider { @@ -15,8 +16,9 @@ export default class DaprApplicationTreeDataProvider extends vscode.Disposable i constructor( private readonly applicationProvider: DaprApplicationProvider, + private readonly daprClient: DaprClient, private readonly installationManager: DaprInstallationManager, - private readonly daprClient: DaprClient) { + private readonly ui: UserInput) { super(() => { this.applicationProviderListener.dispose(); this.onDidChangeTreeDataEmitter.dispose(); @@ -40,16 +42,27 @@ export default class DaprApplicationTreeDataProvider extends vscode.Disposable i if (element) { return element.getChildren?.() ?? []; } else { - const applications = await this.applicationProvider.getApplications(); - const appNodeList = applications.map(application => new DaprApplicationNode(application, this.daprClient)); - + const isInitialized = await this.installationManager.isInitialized(); - if (appNodeList.length > 0) { - return appNodeList; + if (isInitialized) { + await this.ui.executeCommand('setContext', 'vscode-dapr.views.applications.state', 'notRunning'); } else { - return [ new NoApplicationsRunningNode(this.installationManager) ]; + const isInstalled = await this.installationManager.isInstalled(); + + if (isInstalled) { + await this.ui.executeCommand('setContext', 'vscode-dapr.views.applications.state', 'notInitialized'); + } else { + await this.ui.executeCommand('setContext', 'vscode-dapr.views.applications.state', 'notInstalled'); + } } - } + + const applications = await this.applicationProvider.getApplications(); + const appNodeList = applications.map(application => new DaprApplicationNode(application, this.daprClient)); + // NOTE: Returning zero children indicates to VS Code that is should display a "welcome view". + // The one chosen for display depends on the context set above. + + return appNodeList; + } } } \ No newline at end of file diff --git a/src/views/applications/noApplicationsRunningNode.ts b/src/views/applications/noApplicationsRunningNode.ts deleted file mode 100644 index 8907202..0000000 --- a/src/views/applications/noApplicationsRunningNode.ts +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import * as vscode from 'vscode'; -import * as nls from 'vscode-nls'; -import TreeNode from '../treeNode'; -import { DaprInstallationManager } from '../../services/daprInstallationManager'; -import { getLocalizationPathForFile } from '../../util/localization'; - -const localize = nls.loadMessageBundle(getLocalizationPathForFile(__filename)); - -export default class NoApplicationsRunningNode implements TreeNode { - constructor(private readonly installationManager: DaprInstallationManager) { - } - - async getTreeItem(): Promise { - let label = localize('views.applications.noApplicationsRunningNode.notInstalledLabel', 'The Dapr CLI and runtime do not appear to be installed.'); - - const version = await this.installationManager.getVersion(); - - if (version && version.cli) { - label = localize('views.applications.noApplicationsRunningNode.notInitializedLabel', 'The Dapr runtime does not appear to be initialized.'); - - const isInitialized = await this.installationManager.isInitialized(); - - if (isInitialized) { - label = localize('views.applications.noApplicationsRunningNode.notRunning', 'No Dapr applications are running.') - } - } - - const treeItem = new vscode.TreeItem(label); - - treeItem.iconPath = new vscode.ThemeIcon('warning'); - - return Promise.resolve(treeItem); - } -}