diff --git a/package-lock.json b/package-lock.json index 1f95a6be07..1d3a873c76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,12 +44,27 @@ "integrity": "sha512-yOxFfkN9xUFLyvWaeYj90mlqTJ41CsQzWKS3gXdOMOyPVacUsymejKxJ4/pMW7exouubuEeZLJawGgcNGYlTeg==", "dev": true }, + "@types/node-fetch": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.0.tgz", + "integrity": "sha512-TLFRywthBgL68auWj+ziWu+vnmmcHCDFC/sqCOQf1xTz4hRq8cu79z8CtHU9lncExGBsB8fXA4TiLDLt6xvMzw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/rewire": { "version": "2.5.28", "resolved": "https://registry.npmjs.org/@types/rewire/-/rewire-2.5.28.tgz", "integrity": "sha512-uD0j/AQOa5le7afuK+u+woi8jNKF1vf3DN0H7LCJhft/lNNibUr7VcAesdgtWfEKveZol3ZG1CJqwx2Bhrnl8w==", "dev": true }, + "@types/semver": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.0.1.tgz", + "integrity": "sha512-ffCdcrEE5h8DqVxinQjo+2d1q+FV5z7iNtPofw3JsrltSoSVlOGaW0rY8XxtO9XukdTn8TaCGWmk2VFGhI70mg==", + "dev": true + }, "acorn": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", @@ -168,6 +183,13 @@ "requires": { "semver": "^5.3.0", "shimmer": "^1.1.0" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + } } }, "asynckit": { @@ -400,6 +422,13 @@ "async-hook-jl": "^1.7.6", "emitter-listener": "^1.0.1", "semver": "^5.4.1" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + } } }, "co": { @@ -548,6 +577,13 @@ "integrity": "sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc=", "requires": { "semver": "^5.3.0" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + } } }, "diagnostic-channel-publishers": { @@ -700,6 +736,14 @@ "strip-json-comments": "~2.0.1", "table": "4.0.2", "text-table": "~0.2.0" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } } }, "eslint-scope": { @@ -1396,6 +1440,11 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + }, "nth-check": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", @@ -1484,6 +1533,14 @@ "dev": true, "requires": { "semver": "^5.1.0" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } } }, "parse5": { @@ -1749,9 +1806,9 @@ "dev": true }, "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" }, "shebang-command": { "version": "1.2.0", @@ -1953,6 +2010,14 @@ "semver": "^5.3.0", "tslib": "^1.8.0", "tsutils": "^2.29.0" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } } }, "tsutils": { @@ -2104,6 +2169,12 @@ "yazl": "^2.2.2" }, "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + }, "tmp": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz", @@ -2128,6 +2199,14 @@ "source-map-support": "^0.5.0", "url-parse": "^1.4.4", "vscode-test": "^0.4.1" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } } }, "vscode-extension-telemetry": { @@ -2150,6 +2229,13 @@ "requires": { "semver": "^5.5.0", "vscode-languageserver-protocol": "3.14.1" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + } } }, "vscode-languageserver-protocol": { diff --git a/package.json b/package.json index 98d0b37466..1928625794 100644 --- a/package.json +++ b/package.json @@ -40,13 +40,17 @@ "onView:PowerShellCommands" ], "dependencies": { + "node-fetch": "^2.6.0", + "semver": "^6.3.0", "vscode-extension-telemetry": "~0.1.2", "vscode-languageclient": "~5.2.1" }, "devDependencies": { "@types/mocha": "~5.2.7", "@types/node": "~10.11.0", + "@types/node-fetch": "^2.5.0", "@types/rewire": "^2.5.28", + "@types/semver": "^6.0.1", "mocha": "~5.2.0", "mocha-junit-reporter": "~1.23.1", "mocha-multi-reporters": "~1.1.7", @@ -575,6 +579,11 @@ "type": "string", "description": "Specifies the PowerShell version name, as displayed by the 'PowerShell: Show Session Menu' command, used when the extension loads e.g \"Windows PowerShell (x86)\" or \"PowerShell Core 6 (x64)\"." }, + "powershell.promptToUpdatePowerShell": { + "type": "boolean", + "description": "Specifies whether you should be prompted to update your version of PowerShell.", + "default": true + }, "powershell.startAutomatically": { "type": "boolean", "default": true, diff --git a/src/features/UpdatePowerShell.ts b/src/features/UpdatePowerShell.ts new file mode 100644 index 0000000000..ab7caf5a40 --- /dev/null +++ b/src/features/UpdatePowerShell.ts @@ -0,0 +1,146 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import fetch from "node-fetch"; +import * as semver from "semver"; +import { MessageItem, window } from "vscode"; +import { LanguageClient } from "vscode-languageclient"; +import * as Settings from "../settings"; +import { EvaluateRequestType } from "./Console"; + +const PowerShellGitHubReleasesUrl = + "https://api.github.com/repos/PowerShell/PowerShell/releases/latest"; +const PowerShellGitHubPrereleasesUrl = + "https://api.github.com/repos/PowerShell/PowerShell/releases"; + +export class GitHubReleaseInformation { + public static async FetchLatestRelease(preview: boolean): Promise { + // Fetch the latest PowerShell releases from GitHub. + let releaseJson: any; + if (preview) { + // This gets all releases and the first one is the latest prerelease if + // there is a prerelease version. + releaseJson = (await fetch(PowerShellGitHubPrereleasesUrl) + .then((res) => res.json())).find((release: any) => release.prerelease); + } else { + releaseJson = await fetch(PowerShellGitHubReleasesUrl) + .then((res) => res.json()); + } + + return new GitHubReleaseInformation( + releaseJson.tag_name, releaseJson.assets); + } + + public version: semver.SemVer; + public isPreview: boolean = false; + public assets: any[]; + + public constructor(version: string | semver.SemVer, assets: any[] = []) { + this.version = semver.parse(version); + + if (semver.prerelease(this.version)) { + this.isPreview = true; + } + + this.assets = assets; + } +} + +interface IUpdateMessageItem extends MessageItem { + id: number; +} + +export async function InvokePowerShellUpdateCheck( + languageServerClient: LanguageClient, + localVersion: semver.SemVer, + arch: string, + release: GitHubReleaseInformation) { + const options: IUpdateMessageItem[] = [ + { + id: 0, + title: "Yes", + }, + { + id: 1, + title: "Not now", + }, + { + id: 2, + title: "Do not show this notification again", + }, + ]; + + // If our local version is up-to-date, we can return early. + if (semver.compare(localVersion, release.version) >= 0) { + return; + } + + const commonText: string = `You have an old version of PowerShell (${ + localVersion.raw + }). The current latest release is ${ + release.version.raw + }.`; + + if (process.platform === "linux") { + await window.showInformationMessage( + `${commonText} We recommend updating to the latest version.`); + return; + } + + const isMacOS: boolean = process.platform === "darwin"; + const result = await window.showInformationMessage( + `${commonText} Would you like to update the version? ${ + isMacOS ? "(Homebrew is required on macOS)" : "" + }`, ...options); + + // If the user cancels the notification. + if (!result) { return; } + + // Yes choice. + switch (result.id) { + // Yes choice. + case 0: + let script: string; + if (process.platform === "win32") { + const msiMatcher = arch === "x86" ? + "win-x86.msi" : "win-x64.msi"; + + const assetUrl = release.assets.filter((asset: any) => + asset.name.indexOf(msiMatcher) >= 0)[0].browser_download_url; + + // Grab MSI and run it. + // tslint:disable-next-line: max-line-length + script = ` +$randomFileName = [System.IO.Path]::GetRandomFileName() +$tmpMsiPath = Microsoft.PowerShell.Management\\Join-Path ([System.IO.Path]::GetTempPath()) "$randomFileName.msi" +Microsoft.PowerShell.Utility\\Invoke-RestMethod -Uri ${assetUrl} -OutFile $tmpMsiPath +try +{ + Microsoft.PowerShell.Management\\Start-Process -Wait -Path $tmpMsiPath +} +finally +{ + Microsoft.PowerShell.Management\\Remove-Item $tmpMsiPath +}`; + + } else if (isMacOS) { + script = "brew cask upgrade powershell"; + if (release.isPreview) { + script = "brew cask upgrade powershell-preview"; + } + } + + await languageServerClient.sendRequest(EvaluateRequestType, { + expression: script, + }); + break; + + // Never choice. + case 2: + await Settings.change("promptToUpdatePowerShell", false, true); + break; + default: + break; + } +} diff --git a/src/session.ts b/src/session.ts index c110b79577..6b295a2c9a 100644 --- a/src/session.ts +++ b/src/session.ts @@ -3,12 +3,11 @@ *--------------------------------------------------------*/ import cp = require("child_process"); -import crypto = require("crypto"); import fs = require("fs"); import net = require("net"); import os = require("os"); import path = require("path"); -import { StringDecoder } from "string_decoder"; +import * as semver from "semver"; import vscode = require("vscode"); import TelemetryReporter from "vscode-extension-telemetry"; import { Message } from "vscode-jsonrpc"; @@ -23,6 +22,7 @@ import { Middleware, NotificationType, RequestType, RequestType0, ResolveCodeLensSignature, RevealOutputChannelOn, StreamInfo } from "vscode-languageclient"; +import { GitHubReleaseInformation, InvokePowerShellUpdateCheck } from "./features/UpdatePowerShell"; import { fixWindowsPowerShellPath, getAvailablePowerShellExes, getDefaultPowerShellPath, getPlatformDetails, IPlatformDetails, OperatingSystem } from "./platform"; @@ -38,7 +38,6 @@ export enum SessionStatus { export class SessionManager implements Middleware { public HostVersion: string; - private ShowSessionMenuCommandName = "PowerShell.ShowSessionMenu"; private editorServicesArgs: string; private powerShellExePath: string = ""; @@ -586,7 +585,7 @@ export class SessionManager implements Middleware { this.languageServerClient .sendRequest(PowerShellVersionRequestType) .then( - (versionDetails) => { + async (versionDetails) => { this.versionDetails = versionDetails; if (!this.inDevelopmentMode) { @@ -599,6 +598,30 @@ export class SessionManager implements Middleware { ? `${this.versionDetails.displayVersion} (${this.versionDetails.architecture})` : this.versionDetails.displayVersion, SessionStatus.Running); + + // If the user opted to not check for updates, then don't. + if (!this.sessionSettings.promptToUpdatePowerShell) { return; } + + try { + const localVersion = semver.parse(this.versionDetails.version); + if (semver.lt(localVersion, "6.0.0")) { + // Skip prompting when using Windows PowerShell for now. + return; + } + + // Fetch the latest PowerShell releases from GitHub. + const isPreRelease = !!semver.prerelease(localVersion); + const release: GitHubReleaseInformation = + await GitHubReleaseInformation.FetchLatestRelease(isPreRelease); + + await InvokePowerShellUpdateCheck( + this.languageServerClient, + localVersion, + this.versionDetails.architecture, + release); + } catch { + // best effort. This probably failed to fetch the data from GitHub. + } }); // Send the new LanguageClient to extension features diff --git a/src/settings.ts b/src/settings.ts index c537b3a98d..dabd15c90e 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -79,6 +79,7 @@ export interface ISettings { powerShellAdditionalExePaths?: IPowerShellAdditionalExePathSettings[]; powerShellDefaultVersion?: string; powerShellExePath?: string; + promptToUpdatePowerShell?: boolean; bundledModulesPath?: string; startAutomatically?: boolean; useX86Host?: boolean; @@ -167,6 +168,8 @@ export function load(): ISettings { configuration.get("powerShellDefaultVersion", undefined), powerShellExePath: configuration.get("powerShellExePath", undefined), + promptToUpdatePowerShell: + configuration.get("promptToUpdatePowerShell", true), bundledModulesPath: "../../modules", useX86Host: diff --git a/test/features/UpdatePowerShell.test.ts b/test/features/UpdatePowerShell.test.ts new file mode 100644 index 0000000000..20b02b501f --- /dev/null +++ b/test/features/UpdatePowerShell.test.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as assert from "assert"; +import { GitHubReleaseInformation } from "../../src/features/UpdatePowerShell"; + +suite("UpdatePowerShell tests", () => { + test("Can get the latest version", async () => { + const release: GitHubReleaseInformation = await GitHubReleaseInformation.FetchLatestRelease(false); + assert.strictEqual(release.isPreview, false, "expected to not be preview."); + assert.strictEqual(release.version.prerelease.length === 0, true, "expected to not have preview in version."); + assert.strictEqual(release.assets.length > 0, true, "expected to have assets."); + }); + + test("Can get the latest preview version", async () => { + const release: GitHubReleaseInformation = await GitHubReleaseInformation.FetchLatestRelease(true); + assert.strictEqual(release.isPreview, true, "expected to be preview."); + assert.strictEqual(release.version.prerelease.length > 0, true, "expected to have preview in version."); + assert.strictEqual(release.assets.length > 0, true, "expected to have assets."); + }); +});