diff --git a/.gitignore b/.gitignore index 989ccd324e..0a35854e2e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ vscode-powershell.zip vscps-preview.zip *.vsix npm-debug.log +.vscode-test/ diff --git a/.vscode/launch.json b/.vscode/launch.json index 03bcd3cafb..f53cedf458 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,7 @@ "args": [ "--extensionDevelopmentPath=${workspaceRoot}" ], "stopOnEntry": false, "sourceMaps": true, - "outFiles": ["${workspaceRoot}/out"], + "outFiles": ["${workspaceRoot}/out/src/**/*.js"], "preLaunchTask": "BuildAll" }, { @@ -20,7 +20,18 @@ "args": [ "--extensionDevelopmentPath=${workspaceRoot}" ], "stopOnEntry": false, "sourceMaps": true, - "outFiles": ["${workspaceRoot}/out"], + "outFiles": ["${workspaceRoot}/out/src/**/*.js"], + "preLaunchTask": "Build" + }, + { + "name": "Launch Extension Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ], + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": ["${workspaceRoot}/out/test/**/*.js"], "preLaunchTask": "Build" }, { diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8bdc954f97..2bd076fbdc 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,7 +2,7 @@ "version": "2.0.0", "windows": { - "command": "${env:windir}\\sysnative\\WindowsPowerShell\\v1.0\\powershell.exe", + "command": "${env:windir}/System32/WindowsPowerShell/v1.0/powershell.exe", "args": [ "-NoProfile", "-ExecutionPolicy", "Bypass" ] }, "linux": { @@ -20,28 +20,52 @@ { "taskName": "Install", "suppressTaskName": true, - "args": [ "Invoke-Build Restore" ] + "args": [ + "Invoke-Build Restore" + ] }, { "taskName": "CleanAll", "suppressTaskName": true, - "args": [ "Invoke-Build CleanAll" ] + "args": [ + "Invoke-Build CleanAll" + ] }, { "taskName": "Clean", "suppressTaskName": true, - "args": [ "Invoke-Build Clean" ] + "args": [ + "Invoke-Build Clean" + ] }, { "taskName": "BuildAll", "suppressTaskName": true, "isBuildCommand": true, - "args": [ "Invoke-Build BuildAll" ] + "args": [ + "Invoke-Build BuildAll" + ], + "problemMatcher": [] }, { "taskName": "Build", "suppressTaskName": true, - "args": [ "Invoke-Build Build" ] + "args": [ + "Invoke-Build Build" + ], + "problemMatcher": [] + }, + { + "taskName": "Test", + "suppressTaskName": true, + "args": [ + "Invoke-Build Test" + ], + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": [] } ] } diff --git a/.vscodeignore b/.vscodeignore index 76e2e6caf5..3decb35fad 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,4 +1,5 @@ .vscode/** +.vscode-test/** vscode-powershell.build.ps1 typings/** **/*.ts @@ -10,5 +11,7 @@ bin/DebugAdapter.log bin/*.vshost.* bin/PowerShell/** logs/** +out/test/** +test/** sessions/** scripts/Install-VSCode.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d35531630..6a1524d469 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # vscode-powershell Release History +## 1.4.2 +### Tuesday, September 5, 2017 + +- [#993](https://github.com/PowerShell/vscode-powershell/issues/993) - + `powershell.powerShellExePath` using Sysnative path should be automatically + corrected when using 64-bit Visual Studio Code +- [#1008](https://github.com/PowerShell/vscode-powershell/issues/1008) - + Windows PowerShell versions (x64 and x86) are not enumerated correctly + when using 64-bit Visual Studio Code +- [#1009](https://github.com/PowerShell/vscode-powershell/issues/1009) - + PowerShell version indicator in status bar is missing tooltip +- [#1020](https://github.com/PowerShell/vscode-powershell/issues/1020) - + "Show Session Menu", "Show Integrated Console", and "Restart Current Session" + commands should cause PowerShell extension to be activated ## 1.4.1 ### Thursday, June 22, 2017 diff --git a/appveyor.yml b/appveyor.yml index 601c5a6c3e..0f5b13b620 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -version: '1.4.1-insiders-{build}' +version: '1.4.2-insiders-{build}' image: Visual Studio 2017 clone_depth: 10 skip_tags: true diff --git a/examples/.vscode/tasks.json b/examples/.vscode/tasks.json index 6452e9fd81..a9076f42b4 100644 --- a/examples/.vscode/tasks.json +++ b/examples/.vscode/tasks.json @@ -33,7 +33,7 @@ // Start PowerShell "windows": { - "command": "${env:windir}\\sysnative\\WindowsPowerShell\\v1.0\\powershell.exe", + "command": "${env:windir}/System32/WindowsPowerShell/v1.0/powershell.exe", "args": [ "-NoProfile", "-ExecutionPolicy", "Bypass" ] }, "linux": { diff --git a/package.json b/package.json index 64654b2812..f3f4fd1b57 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "PowerShell", "displayName": "PowerShell", - "version": "1.4.1", + "version": "1.4.2", "publisher": "ms-vscode", "description": "Develop PowerShell scripts in Visual Studio Code!", "engines": { @@ -24,7 +24,7 @@ "type": "git", "url": "https://github.com/PowerShell/vscode-powershell.git" }, - "main": "./out/main", + "main": "./out/src/main", "activationEvents": [ "onLanguage:powershell", "onCommand:PowerShell.NewProjectFromTemplate", @@ -32,7 +32,9 @@ "onCommand:PowerShell.StartDebugSession", "onCommand:PowerShell.PickPSHostProcess", "onCommand:PowerShell.SpecifyScriptArgs", - "onCommand:PowerShell.ShowSessionConsole" + "onCommand:PowerShell.ShowSessionConsole", + "onCommand:PowerShell.ShowSessionMenu", + "onCommand:PowerShell.RestartSession" ], "dependencies": { "vscode-languageclient": "3.3.0-alpha.6" @@ -41,7 +43,9 @@ "@types/node": "^6.0.40", "typescript": "^2.0.3", "vsce": "^1.18.0", - "vscode": "^1.1.0" + "vscode": "^1.1.0", + "mocha": "^2.3.3", + "@types/mocha": "^2.2.32" }, "extensionDependencies": [ "vscode.powershell" @@ -49,7 +53,8 @@ "scripts": { "compile": "tsc -p ./", "compile-watch": "tsc -watch -p ./", - "postinstall": "node ./node_modules/vscode/bin/install" + "postinstall": "node ./node_modules/vscode/bin/install", + "test": "node ./node_modules/vscode/bin/test" }, "contributes": { "keybindings": [ @@ -188,7 +193,7 @@ "powershell" ] }, - "program": "./out/debugAdapter.js", + "program": "./out/src/debugAdapter.js", "runtime": "node", "variables": { "PickPSHostProcess": "PowerShell.PickPSHostProcess", diff --git a/src/debugAdapter.ts b/src/debugAdapter.ts index 928973828d..10940dec24 100644 --- a/src/debugAdapter.ts +++ b/src/debugAdapter.ts @@ -14,7 +14,7 @@ import { Logger } from './logging'; // named pipes or a network protocol). It is purely a naive data // relay between the two transports. -var logBasePath = path.resolve(__dirname, "../logs"); +var logBasePath = path.resolve(__dirname, "../../logs"); var debugAdapterLogWriter = fs.createWriteStream( diff --git a/src/features/DocumentFormatter.ts b/src/features/DocumentFormatter.ts index 9bacc9b44f..5a1d299161 100644 --- a/src/features/DocumentFormatter.ts +++ b/src/features/DocumentFormatter.ts @@ -26,6 +26,7 @@ import { } from 'vscode-languageclient'; import { TextDocumentIdentifier } from "vscode-languageserver-types"; import Window = vscode.window; +import { Logger } from '../logging'; import { IFeature } from '../feature'; import * as Settings from '../settings'; import * as Utils from '../utils'; @@ -130,15 +131,17 @@ class PSDocumentFormattingEditProvider implements return Promise.resolve(TextEdit[0]); } - constructor() { + constructor(private logger: Logger) { } provideDocumentFormattingEdits( document: TextDocument, options: FormattingOptions, token: CancellationToken): TextEdit[] | Thenable { - return this.provideDocumentRangeFormattingEdits(document, null, options, token); - } + + this.logger.writeVerbose(`Formatting entire document - ${document.uri}...`) + return this.sendDocumentFormatRequest(document, null, options, token); + } provideDocumentRangeFormattingEdits( document: TextDocument, @@ -146,6 +149,42 @@ class PSDocumentFormattingEditProvider implements options: FormattingOptions, token: CancellationToken): TextEdit[] | Thenable { + this.logger.writeVerbose(`Formatting document range ${JSON.stringify(range)} - ${document.uri}...`) + return this.sendDocumentFormatRequest(document, range, options, token); + } + + provideOnTypeFormattingEdits( + document: TextDocument, + position: Position, + ch: string, + options: FormattingOptions, + token: CancellationToken): TextEdit[] | Thenable { + + this.logger.writeVerbose(`Formatting on type at position ${JSON.stringify(position)} - ${document.uri}...`) + + return this.getScriptRegion(document, position, ch).then(scriptRegion => { + if (scriptRegion === null) { + this.logger.writeVerbose("No formattable range returned."); + return this.emptyPromise; + } + + return this.sendDocumentFormatRequest( + document, + toRange(scriptRegion), + options, + token); + }, + (err) => { + this.logger.writeVerbose(`Error while requesting script region for formatting: ${err}`) + }); + } + + private sendDocumentFormatRequest( + document: TextDocument, + range: Range, + options: FormattingOptions, + token: CancellationToken): TextEdit[] | Thenable { + let editor: TextEditor = this.getEditor(document); if (editor === undefined) { return this.emptyPromise; @@ -157,7 +196,6 @@ class PSDocumentFormattingEditProvider implements return this.emptyPromise; } - // somehow range object gets serialized to an array of Position objects, // so we need to use the object literal syntax to initialize it. let rangeParam = null; @@ -180,31 +218,25 @@ class PSDocumentFormattingEditProvider implements options: this.getEditorSettings() }; + let formattingStartTime = new Date().valueOf(); + function getFormattingDuration() { + return ((new Date().valueOf()) - formattingStartTime) / 1000; + } + let textEdits = this.languageClient.sendRequest( DocumentRangeFormattingRequest.type, requestParams); this.lockDocument(document, textEdits); PSDocumentFormattingEditProvider.showStatusBar(document, textEdits); - return textEdits; - } - - provideOnTypeFormattingEdits( - document: TextDocument, - position: Position, - ch: string, - options: FormattingOptions, - token: CancellationToken): TextEdit[] | Thenable { - return this.getScriptRegion(document, position, ch).then(scriptRegion => { - if (scriptRegion === null) { - return this.emptyPromise; - } - return this.provideDocumentRangeFormattingEdits( - document, - toRange(scriptRegion), - options, - token); - }); + return textEdits.then( + (edits) => { + this.logger.writeVerbose(`Document formatting finished in ${getFormattingDuration()}s`); + return edits; + }, + (err) => { + this.logger.writeVerbose(`Document formatting failed in ${getFormattingDuration()}: ${err}`); + }); } setLanguageClient(languageClient: LanguageClient): void { @@ -284,8 +316,8 @@ export class DocumentFormatterFeature implements IFeature { private languageClient: LanguageClient; private documentFormattingEditProvider: PSDocumentFormattingEditProvider; - constructor() { - this.documentFormattingEditProvider = new PSDocumentFormattingEditProvider(); + constructor(private logger: Logger) { + this.documentFormattingEditProvider = new PSDocumentFormattingEditProvider(logger); this.formattingEditProvider = vscode.languages.registerDocumentFormattingEditProvider( "powershell", this.documentFormattingEditProvider); diff --git a/src/features/Examples.ts b/src/features/Examples.ts index ec436e0b10..77862b6777 100644 --- a/src/features/Examples.ts +++ b/src/features/Examples.ts @@ -12,7 +12,7 @@ export class ExamplesFeature implements IFeature { private examplesPath: string; constructor() { - this.examplesPath = path.resolve(__dirname, "../../examples"); + this.examplesPath = path.resolve(__dirname, "../../../examples"); this.command = vscode.commands.registerCommand('PowerShell.OpenExamplesFolder', () => { vscode.commands.executeCommand( "vscode.openFolder", diff --git a/src/logging.ts b/src/logging.ts index 784301dfca..7691621ea7 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -29,7 +29,7 @@ export class Logger { constructor() { this.logChannel = vscode.window.createOutputChannel("PowerShell Extension Logs"); - this.logBasePath = path.resolve(__dirname, "../logs"); + this.logBasePath = path.resolve(__dirname, "../../logs"); utils.ensurePathExists(this.logBasePath); this.commands = [ @@ -49,10 +49,10 @@ export class Logger { public writeAtLevel(logLevel: LogLevel, message: string, ...additionalMessages: string[]) { if (logLevel >= this.MinimumLogLevel) { - this.writeLine(message) + this.writeLine(message, logLevel) additionalMessages.forEach((line) => { - this.writeLine(line); + this.writeLine(line, logLevel); }); } } @@ -136,11 +136,13 @@ export class Logger { } } - private writeLine(message: string) { - // TODO: Add timestamp - this.logChannel.appendLine(message); + private writeLine(message: string, level: LogLevel = LogLevel.Normal) { + let now = new Date(); + let timestampedMessage = `${now.toLocaleDateString()} ${now.toLocaleTimeString()} [${LogLevel[level].toUpperCase()}] - ${message}` + + this.logChannel.appendLine(timestampedMessage); if (this.logFilePath) { - fs.appendFile(this.logFilePath, message + os.EOL); + fs.appendFile(this.logFilePath, timestampedMessage + os.EOL); } } } diff --git a/src/main.ts b/src/main.ts index 8ccb72bcb4..8204017626 100644 --- a/src/main.ts +++ b/src/main.ts @@ -117,7 +117,7 @@ export function activate(context: vscode.ExtensionContext): void { new SelectPSSARulesFeature(), new CodeActionsFeature(), new NewFileOrProjectFeature(), - new DocumentFormatterFeature(), + new DocumentFormatterFeature(logger), new RemoteFilesFeature(), new DebugSessionFeature(sessionManager), new PickPSHostProcessFeature(), @@ -161,7 +161,7 @@ function checkForUpdatedVersion(context: vscode.ExtensionContext) { if (choice === showReleaseNotes) { vscode.commands.executeCommand( 'markdown.showPreview', - vscode.Uri.file(path.resolve(__dirname, "../CHANGELOG.md"))); + vscode.Uri.file(path.resolve(__dirname, "../../CHANGELOG.md"))); } }); } diff --git a/src/platform.ts b/src/platform.ts new file mode 100644 index 0000000000..adcaae1294 --- /dev/null +++ b/src/platform.ts @@ -0,0 +1,171 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import fs = require('fs'); +import os = require('os'); +import path = require('path'); +import vscode = require('vscode'); +import process = require('process'); +import Settings = require('./settings'); + +export enum OperatingSystem { + Unknown, + Windows, + MacOS, + Linux +} + +export interface PlatformDetails { + operatingSystem: OperatingSystem + isOS64Bit: boolean + isProcess64Bit: boolean +} + +export interface PowerShellExeDetails { + versionName: string; + exePath: string; +} + +export function getPlatformDetails(): PlatformDetails { + var operatingSystem = OperatingSystem.Unknown; + + if (process.platform === "win32") { + operatingSystem = OperatingSystem.Windows; + } + else if (process.platform === "darwin") { + operatingSystem = OperatingSystem.MacOS; + } + else if (process.platform === "linux") { + operatingSystem = OperatingSystem.Linux; + } + + let isProcess64Bit = process.arch === "x64"; + + return { + operatingSystem: operatingSystem, + isOS64Bit: isProcess64Bit || process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'), + isProcess64Bit: isProcess64Bit + } +} + +export function getDefaultPowerShellPath( + platformDetails: PlatformDetails, + use32Bit: boolean = false): string | null { + + var powerShellExePath = undefined; + + // Find the path to powershell.exe based on the current platform + // and the user's desire to run the x86 version of PowerShell + if (platformDetails.operatingSystem == OperatingSystem.Windows) { + if (use32Bit) { + powerShellExePath = + platformDetails.isOS64Bit && platformDetails.isProcess64Bit + ? SysWow64PowerShellPath + : System32PowerShellPath + } + else { + powerShellExePath = + !platformDetails.isOS64Bit || platformDetails.isProcess64Bit + ? System32PowerShellPath + : SysnativePowerShellPath + } + } + else if (platformDetails.operatingSystem == OperatingSystem.MacOS) { + powerShellExePath = "/usr/local/bin/powershell"; + } + else if (platformDetails.operatingSystem == OperatingSystem.Linux) { + powerShellExePath = "/usr/bin/powershell"; + } + + return powerShellExePath; +} + +export function getWindowsSystemPowerShellPath(systemFolderName: string) { + return `${process.env.windir}\\${systemFolderName}\\WindowsPowerShell\\v1.0\\powershell.exe` +} + +export const System32PowerShellPath = getWindowsSystemPowerShellPath('System32'); +export const SysnativePowerShellPath = getWindowsSystemPowerShellPath('Sysnative'); +export const SysWow64PowerShellPath = getWindowsSystemPowerShellPath('SysWow64'); + +export const WindowsPowerShell64BitLabel = "Windows PowerShell (x64)"; +export const WindowsPowerShell32BitLabel = "Windows PowerShell (x86)"; + +const powerShell64BitPathOn32Bit = SysnativePowerShellPath.toLocaleLowerCase(); +const powerShell32BitPathOn64Bit = SysWow64PowerShellPath.toLocaleLowerCase(); + +export function fixWindowsPowerShellPath(powerShellExePath: string, platformDetails: PlatformDetails): string { + let lowerCasedPath = powerShellExePath.toLocaleLowerCase(); + + if ((platformDetails.isProcess64Bit && (lowerCasedPath === powerShell64BitPathOn32Bit)) || + (!platformDetails.isProcess64Bit && (lowerCasedPath === powerShell32BitPathOn64Bit))) { + return System32PowerShellPath; + } + + // If the path doesn't need to be fixed, return the original + return powerShellExePath; +} + +export function getAvailablePowerShellExes(platformDetails: PlatformDetails): PowerShellExeDetails[] { + + var paths: PowerShellExeDetails[] = []; + + if (platformDetails.operatingSystem === OperatingSystem.Windows) { + const psCoreInstallPath = + (!platformDetails.isProcess64Bit ? process.env.ProgramW6432 : process.env.ProgramFiles) + '\\PowerShell'; + + if (platformDetails.isProcess64Bit) { + paths.push({ + versionName: WindowsPowerShell64BitLabel, + exePath: System32PowerShellPath + }) + + paths.push({ + versionName: WindowsPowerShell32BitLabel, + exePath: SysWow64PowerShellPath + }) + } + else { + if (platformDetails.isOS64Bit) { + paths.push({ + versionName: WindowsPowerShell64BitLabel, + exePath: SysnativePowerShellPath + }) + } + + paths.push({ + versionName: WindowsPowerShell32BitLabel, + exePath: System32PowerShellPath + }) + } + + if (fs.existsSync(psCoreInstallPath)) { + var psCorePaths = + fs.readdirSync(psCoreInstallPath) + .map(item => path.join(psCoreInstallPath, item)) + .filter(item => fs.lstatSync(item).isDirectory()) + .map(item => { + return { + versionName: `PowerShell Core ${path.parse(item).base}`, + exePath: path.join(item, "powershell.exe") + }; + }); + + if (psCorePaths) { + paths = paths.concat(psCorePaths); + } + } + } + else { + paths.push({ + versionName: "PowerShell Core", + exePath: + os.platform() === "darwin" + ? "/usr/local/bin/powershell" + : "/usr/bin/powershell" + }); + } + + return paths; +} diff --git a/src/process.ts b/src/process.ts index 99e4b94734..f8d3237328 100644 --- a/src/process.ts +++ b/src/process.ts @@ -40,7 +40,7 @@ export class PowerShellProcess { let startScriptPath = path.resolve( __dirname, - '../scripts/Start-EditorServices.ps1'); + '../../scripts/Start-EditorServices.ps1'); var editorServicesLogPath = this.log.getLogFilePath(logFileName); @@ -77,7 +77,7 @@ export class PowerShellProcess { // NOTE: This batch file approach is needed temporarily until VS Code's // createTerminal API gets an argument for setting environment variables // on the launched process. - var batScriptPath = path.resolve(__dirname, '../sessions/powershell.bat'); + var batScriptPath = path.resolve(__dirname, '../../sessions/powershell.bat'); fs.writeFileSync( batScriptPath, `@set DEVPATH=${path.dirname(powerShellExePath)}\r\n@${powerShellExePath} %*`); @@ -85,7 +85,7 @@ export class PowerShellProcess { powerShellExePath = batScriptPath; } - this.log.write(`${utils.getTimestampString()} Language server starting...`); + this.log.write("Language server starting..."); // Make sure no old session file exists utils.deleteSessionFile(this.sessionFilePath); diff --git a/src/session.ts b/src/session.ts index f6fde9de1f..417d0aaf83 100644 --- a/src/session.ts +++ b/src/session.ts @@ -16,12 +16,18 @@ import { IFeature } from './feature'; import { Message } from 'vscode-jsonrpc'; import { PowerShellProcess } from './process'; import { StringDecoder } from 'string_decoder'; + import { LanguageClient, LanguageClientOptions, Executable, RequestType, RequestType0, NotificationType, StreamInfo, ErrorAction, CloseAction, RevealOutputChannelOn, Middleware, ResolveCodeLensSignature } from 'vscode-languageclient'; +import { + OperatingSystem, PlatformDetails, getDefaultPowerShellPath, + getPlatformDetails, fixWindowsPowerShellPath, + getAvailablePowerShellExes } from './platform'; + export enum SessionStatus { NotStarted, Initializing, @@ -35,12 +41,12 @@ export class SessionManager implements Middleware { private ShowSessionMenuCommandName = "PowerShell.ShowSessionMenu"; private hostVersion: string; - private isWindowsOS: boolean; private editorServicesArgs: string; private powerShellExePath: string = ""; private sessionStatus: SessionStatus; private suppressRestartPrompt: boolean; private focusConsoleOnExecute: boolean; + private platformDetails: PlatformDetails; private extensionFeatures: IFeature[] = []; private statusBarItem: vscode.StatusBarItem; private languageServerProcess: PowerShellProcess; @@ -61,7 +67,7 @@ export class SessionManager implements Middleware { private requiredEditorServicesVersion: string, private log: Logger) { - this.isWindowsOS = os.platform() == "win32"; + this.platformDetails = getPlatformDetails(); // Get the current version of this extension this.hostVersion = @@ -71,6 +77,14 @@ export class SessionManager implements Middleware { .packageJSON .version; + let osBitness = this.platformDetails.isOS64Bit ? "64-bit" : "32-bit"; + let procBitness = this.platformDetails.isProcess64Bit ? "64-bit" : "32-bit"; + + this.log.write( + `Visual Studio Code v${vscode.version} ${procBitness}`, + `PowerShell Extension v${this.hostVersion}`, + `Operating System: ${OperatingSystem[this.platformDetails.operatingSystem]} ${osBitness}\n`); + // Fix the host version so that PowerShell can consume it. // This is needed when the extension uses a prerelease // version string like 0.9.1-insiders-1234. @@ -121,7 +135,7 @@ export class SessionManager implements Middleware { if (this.powerShellExePath) { - var bundledModulesPath = path.resolve(__dirname, "../modules"); + var bundledModulesPath = path.resolve(__dirname, "../../modules"); if (this.inDevelopmentMode) { var devBundledModulesPath = @@ -129,7 +143,7 @@ export class SessionManager implements Middleware { path.resolve( __dirname, this.sessionSettings.developer.bundledModulesPath || - "../../PowerShellEditorServices/module"); + "../../../PowerShellEditorServices/module"); // Make sure the module's bin path exists if (fs.existsSync(path.join(devBundledModulesPath, "PowerShellEditorServices/bin"))) { @@ -315,7 +329,7 @@ export class SessionManager implements Middleware { this.sessionDetails = sessionDetails; if (sessionDetails.status === "started") { - this.log.write(`${utils.getTimestampString()} Language server started.`); + this.log.write("Language server started."); // Start the language service client this.startLanguageClient(sessionDetails); @@ -338,7 +352,7 @@ export class SessionManager implements Middleware { } }, error => { - this.log.write(`${utils.getTimestampString()} Language server startup failed.`); + this.log.write("Language server startup failed."); this.setSessionFailure("The language service could not be started: ", error); } ); @@ -460,6 +474,7 @@ export class SessionManager implements Middleware { 1); this.statusBarItem.command = this.ShowSessionMenuCommandName; + this.statusBarItem.tooltip = "Show PowerShell Session Menu"; this.statusBarItem.show(); vscode.window.onDidChangeActiveTextEditor(textEditor => { if (textEditor === undefined @@ -541,9 +556,53 @@ export class SessionManager implements Middleware { this.sessionSettings.developer.powerShellExePath || "").trim(); + if (this.platformDetails.operatingSystem === OperatingSystem.Windows && + powerShellExePath.length > 0) { + + // Check the path bitness + let fixedPath = + fixWindowsPowerShellPath( + powerShellExePath, + this.platformDetails); + + if (fixedPath !== powerShellExePath) { + let bitness = this.platformDetails.isOS64Bit ? 64 : 32; + // Show deprecation message with fix action. + // We don't need to wait on this to complete + // because we can finish gathering the configured + // PowerShell path without the fix + vscode + .window + .showWarningMessage( + `The specified PowerShell path is incorrect for ${bitness}-bit VS Code, using '${fixedPath}' instead.`, + "Fix Setting Automatically") + .then(choice => { + if (choice) { + this.suppressRestartPrompt = true; + Settings + .change( + "powerShellExePath", + this.sessionSettings.developer.powerShellExePath, + true) + .then(() => { + return Settings.change( + "developer.powerShellExePath", + undefined, + true) + }) + .then(() => { + this.suppressRestartPrompt = false; + }); + } + }); + + powerShellExePath = fixedPath; + } + } + return powerShellExePath.length > 0 ? this.resolvePowerShellPath(powerShellExePath) - : this.getDefaultPowerShellPath(this.sessionSettings.useX86Host); + : getDefaultPowerShellPath(this.platformDetails, this.sessionSettings.useX86Host); } private changePowerShellExePath(exePath: string) { @@ -553,78 +612,6 @@ export class SessionManager implements Middleware { .then(() => this.restartSession()); } - private getPowerShellExeItems(): PowerShellExeDetails[] { - - var paths: PowerShellExeDetails[] = []; - - if (this.isWindowsOS) { - const is64Bit = process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'); - const rootInstallPath = (is64Bit ? process.env.ProgramW6432 : process.env.ProgramFiles) + '\\PowerShell'; - - if (fs.existsSync(rootInstallPath)) { - var psCorePaths = - fs.readdirSync(rootInstallPath) - .map(item => path.join(rootInstallPath, item)) - .filter(item => fs.lstatSync(item).isDirectory()) - .map(item => { - return { - versionName: `PowerShell Core ${path.parse(item).base}`, - exePath: path.join(item, "powershell.exe") - }; - }); - - if (psCorePaths) { - paths = paths.concat(psCorePaths); - } - } - - if (is64Bit) { - paths.push({ - versionName: "Windows PowerShell (x64)", - exePath: process.env.windir + '\\Sysnative\\WindowsPowerShell\\v1.0\\powershell.exe' - }) - } - - paths.push({ - versionName: "Windows PowerShell (x86)", - exePath: process.env.windir + '\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' - }) - } - else { - paths.push({ - versionName: "PowerShell Core", - exePath: - os.platform() === "darwin" - ? "/usr/local/bin/powershell" - : "/usr/bin/powershell" - }); - } - - return paths; - } - - private getDefaultPowerShellPath(use32Bit: boolean): string | null { - - // Find the path to powershell.exe based on the current platform - // and the user's desire to run the x86 version of PowerShell - var powerShellExePath = undefined; - - if (this.isWindowsOS) { - powerShellExePath = - use32Bit || !process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432') - ? process.env.windir + '\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' - : process.env.windir + '\\Sysnative\\WindowsPowerShell\\v1.0\\powershell.exe'; - } - else if (os.platform() == "darwin") { - powerShellExePath = "/usr/local/bin/powershell"; - } - else { - powerShellExePath = "/usr/bin/powershell"; - } - - return this.resolvePowerShellPath(powerShellExePath); - } - private resolvePowerShellPath(powerShellExePath: string): string { var resolvedPath = path.resolve(__dirname, powerShellExePath); @@ -649,14 +636,6 @@ export class SessionManager implements Middleware { private showSessionMenu() { var menuItems: SessionMenuItem[] = []; - if (this.sessionStatus === SessionStatus.Initializing || - this.sessionStatus === SessionStatus.NotStarted || - this.sessionStatus === SessionStatus.Stopping) { - - // Don't show a menu for these states - return; - } - if (this.sessionStatus === SessionStatus.Running) { menuItems = [ new SessionMenuItem( @@ -676,9 +655,9 @@ export class SessionManager implements Middleware { ]; } - var currentExePath = this.powerShellExePath.toLowerCase(); + var currentExePath = (this.powerShellExePath || "").toLowerCase(); var powerShellItems = - this.getPowerShellExeItems() + getAvailablePowerShellExes(this.platformDetails) .filter(item => item.exePath.toLowerCase() !== currentExePath) .map(item => { return new SessionMenuItem( @@ -747,11 +726,6 @@ export class SessionManager implements Middleware { } } -interface PowerShellExeDetails { - versionName: string; - exePath: string; -} - class SessionMenuItem implements vscode.QuickPickItem { public description: string; diff --git a/src/utils.ts b/src/utils.ts index 1fa9e0f30a..edffeb1d90 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -52,7 +52,7 @@ export interface WaitForSessionFileCallback { (details: EditorServicesSessionDetails, error: string): void; } -let sessionsFolder = path.resolve(__dirname, "..", "sessions/"); +let sessionsFolder = path.resolve(__dirname, "..", "..", "sessions/"); let sessionFilePathPrefix = path.resolve(sessionsFolder, "PSES-VSCode-" + process.env.VSCODE_PID); // Create the sessions path if it doesn't exist already diff --git a/test/index.ts b/test/index.ts new file mode 100644 index 0000000000..1d6519d0d3 --- /dev/null +++ b/test/index.ts @@ -0,0 +1,9 @@ +var testRunner = require('vscode/lib/testrunner'); + +// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for options +testRunner.configure({ + ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) + useColors: true // colored output from test results +}); + +module.exports = testRunner; diff --git a/test/platform.test.ts b/test/platform.test.ts new file mode 100644 index 0000000000..b115451e77 --- /dev/null +++ b/test/platform.test.ts @@ -0,0 +1,121 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import * as platform from '../src/platform'; + +function checkDefaultPowerShellPath(platformDetails, expectedPath) { + test("returns correct default path", () => { + assert.equal( + platform.getDefaultPowerShellPath(platformDetails), + expectedPath); + }); +} + +function checkAvailableWindowsPowerShellPaths( + platformDetails: platform.PlatformDetails, + expectedPaths: platform.PowerShellExeDetails[]) { + test("correctly enumerates available Windows PowerShell paths", () => { + + // The system may return PowerShell Core paths so only + // enumerate the first list items. + let enumeratedPaths = platform.getAvailablePowerShellExes(platformDetails); + for (var i; i < expectedPaths.length; i++) { + assert.equal(enumeratedPaths[i], expectedPaths[i]); + } + }); +} + +function checkFixedWindowsPowerShellpath(platformDetails, inputPath, expectedPath) { + test("fixes incorrect Windows PowerShell Sys* path", () => { + assert.equal( + platform.fixWindowsPowerShellPath(inputPath, platformDetails), + expectedPath); + }); +} + +suite("Platform module", () => { + + suite("64-bit Windows, 64-bit VS Code", () => { + let platformDetails: platform.PlatformDetails = { + operatingSystem: platform.OperatingSystem.Windows, + isOS64Bit: true, + isProcess64Bit: true + }; + + checkDefaultPowerShellPath( + platformDetails, + platform.System32PowerShellPath); + + checkAvailableWindowsPowerShellPaths( + platformDetails, + [ + { + versionName: platform.WindowsPowerShell64BitLabel, + exePath: platform.System32PowerShellPath + }, + { + versionName: platform.WindowsPowerShell32BitLabel, + exePath: platform.SysWow64PowerShellPath + } + ]); + + checkFixedWindowsPowerShellpath( + platformDetails, + platform.SysnativePowerShellPath, + platform.System32PowerShellPath); + }); + + suite("64-bit Windows, 32-bit VS Code", () => { + let platformDetails: platform.PlatformDetails = { + operatingSystem: platform.OperatingSystem.Windows, + isOS64Bit: true, + isProcess64Bit: false + }; + + checkDefaultPowerShellPath( + platformDetails, + platform.SysnativePowerShellPath); + + checkAvailableWindowsPowerShellPaths( + platformDetails, + [ + { + versionName: platform.WindowsPowerShell64BitLabel, + exePath: platform.SysnativePowerShellPath + }, + { + versionName: platform.WindowsPowerShell32BitLabel, + exePath: platform.System32PowerShellPath + } + ]); + + checkFixedWindowsPowerShellpath( + platformDetails, + platform.SysWow64PowerShellPath, + platform.System32PowerShellPath); + }); + + suite("32-bit Windows, 32-bit VS Code", () => { + let platformDetails: platform.PlatformDetails = { + operatingSystem: platform.OperatingSystem.Windows, + isOS64Bit: false, + isProcess64Bit: false + }; + + checkDefaultPowerShellPath( + platformDetails, + platform.System32PowerShellPath); + + checkAvailableWindowsPowerShellPaths( + platformDetails, + [ + { + versionName: platform.WindowsPowerShell32BitLabel, + exePath: platform.System32PowerShellPath + } + ]); + }); +}); diff --git a/vscode-powershell.build.ps1 b/vscode-powershell.build.ps1 index bd3163be30..43b4c20693 100644 --- a/vscode-powershell.build.ps1 +++ b/vscode-powershell.build.ps1 @@ -92,6 +92,16 @@ task BuildEditorServices { task BuildAll BuildEditorServices, Build -Before Package +task Test Build, { + if (!$global:IsLinux -and !$global:IsOSX) { + Write-Host "`n### Running extension tests" -ForegroundColor Green + exec { & npm run test } + } + else { + Write-Host "`n### Skipping extension tests on non-Windows platform" -ForegroundColor Yellow + } +} + task Package { if ($script:psesBuildScriptPath) { @@ -112,4 +122,4 @@ task UploadArtifacts -If { $env:AppVeyor } { } # The default task is to run the entire CI build -task . GetExtensionVersion, CleanAll, BuildAll, Package, UploadArtifacts +task . GetExtensionVersion, CleanAll, BuildAll, Test, Package, UploadArtifacts