diff --git a/.gitignore b/.gitignore index 29133a214d..989ccd324e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,9 @@ out/ node_modules/ logs/ modules/* +sessions/* !modules/README.md vscode-powershell.zip vscps-preview.zip -*.vsix \ No newline at end of file +*.vsix +npm-debug.log diff --git a/.vscode/settings.json b/.vscode/settings.json index 03dbb9ed1d..e16db48f31 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,8 @@ "editor.insertSpaces": true, // When enabled, will trim trailing whitespace when you save a file. - "files.trimTrailingWhitespace": true + "files.trimTrailingWhitespace": true, + + // Lock the TypeScript SDK path to the version we use + "typescript.tsdk": "./node_modules/typescript/lib" } \ No newline at end of file diff --git a/.vscodeignore b/.vscodeignore index e22b8f6bbc..517b97bdd6 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -8,4 +8,4 @@ bin/EditorServices.log bin/DebugAdapter.log bin/*.vshost.* logs/** - +sessions/** diff --git a/examples/.vscode/launch.json b/examples/.vscode/launch.json index 0b7b52fe44..594726a1d5 100644 --- a/examples/.vscode/launch.json +++ b/examples/.vscode/launch.json @@ -10,4 +10,4 @@ "cwd": "${file}" } ] -} +} \ No newline at end of file diff --git a/package.json b/package.json index a88d2e4059..fd1705bf5f 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "publisher": "ms-vscode", "description": "Develop PowerShell scripts in Visual Studio Code!", "engines": { - "vscode": "1.x.x" + "vscode": "^1.7.0" }, "license": "SEE LICENSE IN LICENSE.txt", "homepage": "https://github.com/PowerShell/vscode-powershell/blob/master/README.md", @@ -32,16 +32,17 @@ "vscode-languageclient": "1.3.1" }, "devDependencies": { - "vscode": "^0.11.12", - "typescript": "^1.8.0" + "vscode": "^1.0.0", + "typescript": "^2.0.3", + "@types/node": "^6.0.40" }, "extensionDependencies": [ "vscode.powershell" ], "scripts": { - "vscode:prepublish": "node ./node_modules/vscode/bin/compile", - "compile": "node ./node_modules/vscode/bin/compile -p ./", - "compile-watch": "node ./node_modules/vscode/bin/compile -watch -p ./", + "vscode:prepublish": "tsc -p ./", + "compile": "tsc -p ./", + "compile-watch": "tsc -watch -p ./", "postinstall": "node ./node_modules/vscode/bin/install" }, "contributes": { @@ -88,6 +89,21 @@ "title": "Run selection", "category": "PowerShell" }, + { + "command": "PowerShell.RestartSession", + "title": "Restart Current Session", + "category": "PowerShell" + }, + { + "command": "PowerShell.ShowLogs", + "title": "Show PowerShell Extension Logs", + "category": "PowerShell" + }, + { + "command": "PowerShell.OpenLogFolder", + "title": "Open PowerShell Extension Logs Folder", + "category": "PowerShell" + }, { "command": "PowerShell.OpenInISE", "title": "Open current file in PowerShell ISE", @@ -102,6 +118,11 @@ "command": "PowerShell.ShowAdditionalCommands", "title": "Show additional commands from PowerShell modules", "category": "PowerShell" + }, + { + "command": "PowerShell.ShowSessionMenu", + "title": "Show Session Menu", + "category": "PowerShell" } ], "snippets": [ diff --git a/scripts/Start-EditorServices.ps1 b/scripts/Start-EditorServices.ps1 index f447f20208..47a42bb330 100644 --- a/scripts/Start-EditorServices.ps1 +++ b/scripts/Start-EditorServices.ps1 @@ -57,6 +57,12 @@ param( # Are we running in PowerShell 5 or later? $isPS5orLater = $PSVersionTable.PSVersion.Major -ge 5 +# If PSReadline is present in the session, remove it so that runspace +# management is easier +if ((Get-Module PSReadline).Count -ne 0) { + Remove-Module PSReadline +} + # This variable will be assigned later to contain information about # what happened while attempting to launch the PowerShell Editor # Services host @@ -161,6 +167,7 @@ else { $languageServicePort = Get-AvailablePort $debugServicePort = Get-AvailablePort +# Create the Editor Services host $editorServicesHost = Start-EditorServicesHost ` -HostName $HostName ` diff --git a/src/debugAdapter.ts b/src/debugAdapter.ts index 6a9484cd9d..44fe370a06 100644 --- a/src/debugAdapter.ts +++ b/src/debugAdapter.ts @@ -1,8 +1,12 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + import fs = require('fs'); import path = require('path'); import net = require('net'); import utils = require('./utils'); -import logging = require('./logging'); +import { Logger } from './logging'; // NOTE: The purpose of this file is to serve as a bridge between // VS Code's debug adapter client (which communicates via stdio) and @@ -11,13 +15,12 @@ import logging = require('./logging'); // relay between the two transports. var logBasePath = path.resolve(__dirname, "../logs"); -utils.ensurePathExists(logBasePath); var debugAdapterLogWriter = fs.createWriteStream( path.resolve( logBasePath, - logging.getLogName("DebugAdapterClient"))); + "DebugAdapter.log")); // Pause the stdin buffer until we're connected to the // debug server diff --git a/src/feature.ts b/src/feature.ts new file mode 100644 index 0000000000..596f0cc8ed --- /dev/null +++ b/src/feature.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import vscode = require('vscode'); +import { LanguageClient } from 'vscode-languageclient'; +export { LanguageClient } from 'vscode-languageclient'; + +export interface IFeature extends vscode.Disposable { + setLanguageClient(languageclient: LanguageClient); + dispose(); +} diff --git a/src/features/Console.ts b/src/features/Console.ts index 3b3f5fe65d..5cf155cb9a 100644 --- a/src/features/Console.ts +++ b/src/features/Console.ts @@ -1,4 +1,9 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + import vscode = require('vscode'); +import { IFeature } from '../feature'; import { LanguageClient, RequestType, NotificationType } from 'vscode-languageclient'; export namespace EvaluateRequest { @@ -138,44 +143,66 @@ function onInputEntered(responseText: string): ShowInputPromptResponseBody { } } -export function registerConsoleCommands(client: LanguageClient): void { +export class ConsoleFeature implements IFeature { + private command: vscode.Disposable; + private languageClient: LanguageClient; + private consoleChannel: vscode.OutputChannel; + + constructor() { + this.command = + vscode.commands.registerCommand('PowerShell.RunSelection', () => { + if (this.languageClient === undefined) { + // TODO: Log error message + return; + } + + var editor = vscode.window.activeTextEditor; + var selectionRange: vscode.Range = undefined; + + if (!editor.selection.isEmpty) { + selectionRange = + new vscode.Range( + editor.selection.start, + editor.selection.end); + } + else { + selectionRange = editor.document.lineAt(editor.selection.start.line).range; + } + + this.languageClient.sendRequest(EvaluateRequest.type, { + expression: editor.document.getText(selectionRange) + }); + }); + + this.consoleChannel = vscode.window.createOutputChannel("PowerShell Output"); + } - vscode.commands.registerCommand('PowerShell.RunSelection', () => { - var editor = vscode.window.activeTextEditor; - var selectionRange: vscode.Range = undefined; + public setLanguageClient(languageClient: LanguageClient) { + this.languageClient = languageClient; - if (!editor.selection.isEmpty) { - selectionRange = - new vscode.Range( - editor.selection.start, - editor.selection.end); - } - else { - selectionRange = editor.document.lineAt(editor.selection.start.line).range; - } + this.languageClient.onRequest( + ShowChoicePromptRequest.type, + promptDetails => showChoicePrompt(promptDetails, this.languageClient)); - client.sendRequest(EvaluateRequest.type, { - expression: editor.document.getText(selectionRange) - }); - }); + this.languageClient.onRequest( + ShowInputPromptRequest.type, + promptDetails => showInputPrompt(promptDetails, this.languageClient)); - var consoleChannel = vscode.window.createOutputChannel("PowerShell Output"); - client.onNotification(OutputNotification.type, (output) => { - var outputEditorExist = vscode.window.visibleTextEditors.some((editor) => { - return editor.document.languageId == 'Log' - }); - if (!outputEditorExist) - consoleChannel.show(vscode.ViewColumn.Three); - consoleChannel.append(output.output); - }); + this.languageClient.onNotification(OutputNotification.type, (output) => { + var outputEditorExist = vscode.window.visibleTextEditors.some((editor) => { + return editor.document.languageId == 'Log' + }); - var t: Thenable; + if (!outputEditorExist) { + this.consoleChannel.show(vscode.ViewColumn.Three); + } - client.onRequest( - ShowChoicePromptRequest.type, - promptDetails => showChoicePrompt(promptDetails, client)); + this.consoleChannel.append(output.output); + }); + } - client.onRequest( - ShowInputPromptRequest.type, - promptDetails => showInputPrompt(promptDetails, client)); + public dispose() { + this.command.dispose(); + this.consoleChannel.dispose(); + } } diff --git a/src/features/ExpandAlias.ts b/src/features/ExpandAlias.ts index f8cfc73662..e4997daa97 100644 --- a/src/features/ExpandAlias.ts +++ b/src/features/ExpandAlias.ts @@ -1,37 +1,59 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + import vscode = require('vscode'); -import { LanguageClient, RequestType, NotificationType } from 'vscode-languageclient'; import Window = vscode.window; +import { IFeature } from '../feature'; +import { LanguageClient, RequestType, NotificationType } from 'vscode-languageclient'; export namespace ExpandAliasRequest { export const type: RequestType = { get method() { return 'powerShell/expandAlias'; } }; } -export function registerExpandAliasCommand(client: LanguageClient): void { - var disposable = vscode.commands.registerCommand('PowerShell.ExpandAlias', () => { - - var editor = Window.activeTextEditor; - var document = editor.document; - var selection = editor.selection; - var text, range; - - var sls = selection.start; - var sle = selection.end; - - if ( - (sls.character === sle.character) && - (sls.line === sle.line) - ) { - text = document.getText(); - range = new vscode.Range(0, 0, document.lineCount, text.length); - } else { - text = document.getText(selection); - range = new vscode.Range(sls.line, sls.character, sle.line, sle.character); - } - - client.sendRequest(ExpandAliasRequest.type, text).then((result) => { - editor.edit((editBuilder) => { - editBuilder.replace(range, result); +export class ExpandAliasFeature implements IFeature { + private command: vscode.Disposable; + private languageClient: LanguageClient; + + constructor() { + this.command = vscode.commands.registerCommand('PowerShell.ExpandAlias', () => { + if (this.languageClient === undefined) { + // TODO: Log error message + return; + } + + var editor = Window.activeTextEditor; + var document = editor.document; + var selection = editor.selection; + var text, range; + + var sls = selection.start; + var sle = selection.end; + + if ( + (sls.character === sle.character) && + (sls.line === sle.line) + ) { + text = document.getText(); + range = new vscode.Range(0, 0, document.lineCount, text.length); + } else { + text = document.getText(selection); + range = new vscode.Range(sls.line, sls.character, sle.line, sle.character); + } + + this.languageClient.sendRequest(ExpandAliasRequest.type, text).then((result) => { + editor.edit((editBuilder) => { + editBuilder.replace(range, result); + }); }); }); - }); + } + + public setLanguageClient(languageclient: LanguageClient) { + this.languageClient = languageclient; + } + + public dispose() { + this.command.dispose(); + } } \ No newline at end of file diff --git a/src/features/ExtensionCommands.ts b/src/features/ExtensionCommands.ts index 9e646feb12..9e81d4bf38 100644 --- a/src/features/ExtensionCommands.ts +++ b/src/features/ExtensionCommands.ts @@ -1,5 +1,10 @@ -import vscode = require('vscode'); +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + import path = require('path'); +import vscode = require('vscode'); +import { IFeature } from '../feature'; import { LanguageClient, RequestType, NotificationType, Range, Position } from 'vscode-languageclient'; export interface ExtensionCommand { @@ -11,8 +16,6 @@ export interface ExtensionCommandQuickPickItem extends vscode.QuickPickItem { command: ExtensionCommand; } -var extensionCommands: ExtensionCommand[] = []; - export namespace InvokeExtensionCommandRequest { export const type: RequestType = { get method() { return 'powerShell/invokeExtensionCommand'; } }; @@ -39,52 +42,6 @@ export interface ExtensionCommandAddedNotificationBody { displayName: string; } -function addExtensionCommand(command: ExtensionCommandAddedNotificationBody) { - - extensionCommands.push({ - name: command.name, - displayName: command.displayName - }); -} - -function showExtensionCommands(client: LanguageClient) : Thenable { - - // If no extension commands are available, show a message - if (extensionCommands.length == 0) { - vscode.window.showInformationMessage( - "No extension commands have been loaded into the current session."); - - return; - } - - var quickPickItems = - extensionCommands.map(command => { - return { - label: command.displayName, - description: "", - command: command - } - }); - - vscode.window - .showQuickPick( - quickPickItems, - { placeHolder: "Select a command" }) - .then(command => onCommandSelected(command, client)); -} - -function onCommandSelected( - chosenItem: ExtensionCommandQuickPickItem, - client: LanguageClient) { - - if (chosenItem !== undefined) { - client.sendRequest( - InvokeExtensionCommandRequest.type, - { name: chosenItem.command.name, - context: getEditorContext() }); - } -} - // ---------- Editor Operations ---------- function asRange(value: vscode.Range): Range { @@ -107,8 +64,7 @@ function asPosition(value: vscode.Position): Position { return { line: value.line, character: value.character }; } - -export function asCodeRange(value: Range): vscode.Range { +function asCodeRange(value: Range): vscode.Range { if (value === undefined) { return undefined; @@ -118,7 +74,7 @@ export function asCodeRange(value: Range): vscode.Range { return new vscode.Range(asCodePosition(value.start), asCodePosition(value.end)); } -export function asCodePosition(value: Position): vscode.Position { +function asCodePosition(value: Position): vscode.Position { if (value === undefined) { return undefined; @@ -128,18 +84,6 @@ export function asCodePosition(value: Position): vscode.Position { return new vscode.Position(value.line, value.character); } -function getEditorContext(): EditorContext { - return { - currentFilePath: vscode.window.activeTextEditor.document.fileName, - cursorPosition: asPosition(vscode.window.activeTextEditor.selection.active), - selectionRange: - asRange( - new vscode.Range( - vscode.window.activeTextEditor.selection.start, - vscode.window.activeTextEditor.selection.end)) - } -} - export namespace GetEditorContextRequest { export const type: RequestType = { get method() { return 'editor/getEditorContext'; } }; @@ -164,27 +108,6 @@ export interface InsertTextRequestArguments { insertRange: Range } -function insertText(details: InsertTextRequestArguments): EditorOperationResponse { - var edit = new vscode.WorkspaceEdit(); - - edit.set( - vscode.Uri.parse(details.filePath), - [ - new vscode.TextEdit( - new vscode.Range( - details.insertRange.start.line, - details.insertRange.start.character, - details.insertRange.end.line, - details.insertRange.end.character), - details.insertText) - ] - ); - - vscode.workspace.applyEdit(edit); - - return EditorOperationResponse.Completed; -} - export namespace SetSelectionRequest { export const type: RequestType = { get method() { return 'editor/setSelection'; } }; @@ -194,69 +117,172 @@ export interface SetSelectionRequestArguments { selectionRange: Range } -function setSelection(details: SetSelectionRequestArguments): EditorOperationResponse { - vscode.window.activeTextEditor.selections = [ - new vscode.Selection( - asCodePosition(details.selectionRange.start), - asCodePosition(details.selectionRange.end)) - ] - - return EditorOperationResponse.Completed; -} - export namespace OpenFileRequest { export const type: RequestType = { get method() { return 'editor/openFile'; } }; } -function openFile(filePath: string): Thenable { +export class ExtensionCommandsFeature implements IFeature { - // Make sure the file path is absolute - if (!path.win32.isAbsolute(filePath)) - { - filePath = path.win32.resolve( - vscode.workspace.rootPath, - filePath); + private command: vscode.Disposable; + private languageClient: LanguageClient; + private extensionCommands = []; + + constructor() { + this.command = vscode.commands.registerCommand('PowerShell.ShowAdditionalCommands', () => { + if (this.languageClient === undefined) { + // TODO: Log error message + return; + } + + var editor = vscode.window.activeTextEditor; + var start = editor.selection.start; + var end = editor.selection.end; + if (editor.selection.isEmpty) { + start = new vscode.Position(start.line, 0) + } + + this.showExtensionCommands(this.languageClient); + }); } - var promise = - vscode.workspace.openTextDocument(filePath) - .then(doc => vscode.window.showTextDocument(doc)) - .then(_ => EditorOperationResponse.Completed); + public setLanguageClient(languageclient: LanguageClient) { + // Clear the current list of extension commands since they were + // only relevant to the previous session + this.extensionCommands = []; - return promise; -} + this.languageClient = languageclient; + if (this.languageClient !== undefined) { + this.languageClient.onNotification( + ExtensionCommandAddedNotification.type, + command => this.addExtensionCommand(command)); -export function registerExtensionCommands(client: LanguageClient): void { + this.languageClient.onRequest( + GetEditorContextRequest.type, + details => this.getEditorContext()); - vscode.commands.registerCommand('PowerShell.ShowAdditionalCommands', () => { - var editor = vscode.window.activeTextEditor; - var start = editor.selection.start; - var end = editor.selection.end; - if (editor.selection.isEmpty) { - start = new vscode.Position(start.line, 0) + this.languageClient.onRequest( + InsertTextRequest.type, + details => this.insertText(details)); + + this.languageClient.onRequest( + SetSelectionRequest.type, + details => this.setSelection(details)); + + this.languageClient.onRequest( + OpenFileRequest.type, + filePath => this.openFile(filePath)); } + } - showExtensionCommands(client); - }); + public dispose() { + this.command.dispose(); + } - client.onNotification( - ExtensionCommandAddedNotification.type, - command => addExtensionCommand(command)); + private addExtensionCommand(command: ExtensionCommandAddedNotificationBody) { - client.onRequest( - GetEditorContextRequest.type, - details => getEditorContext()); + this.extensionCommands.push({ + name: command.name, + displayName: command.displayName + }); + } + + private showExtensionCommands(client: LanguageClient) : Thenable { + + // If no extension commands are available, show a message + if (this.extensionCommands.length == 0) { + vscode.window.showInformationMessage( + "No extension commands have been loaded into the current session."); - client.onRequest( - InsertTextRequest.type, - details => insertText(details)); + return; + } + + var quickPickItems = + this.extensionCommands.map(command => { + return { + label: command.displayName, + description: "", + command: command + } + }); + + vscode.window + .showQuickPick( + quickPickItems, + { placeHolder: "Select a command" }) + .then(command => this.onCommandSelected(command, client)); + } - client.onRequest( - SetSelectionRequest.type, - details => setSelection(details)); + private onCommandSelected( + chosenItem: ExtensionCommandQuickPickItem, + client: LanguageClient) { - client.onRequest( - OpenFileRequest.type, - filePath => openFile(filePath)); + if (chosenItem !== undefined) { + client.sendRequest( + InvokeExtensionCommandRequest.type, + { name: chosenItem.command.name, + context: this.getEditorContext() }); + } + } + + private insertText(details: InsertTextRequestArguments): EditorOperationResponse { + var edit = new vscode.WorkspaceEdit(); + + edit.set( + vscode.Uri.parse(details.filePath), + [ + new vscode.TextEdit( + new vscode.Range( + details.insertRange.start.line, + details.insertRange.start.character, + details.insertRange.end.line, + details.insertRange.end.character), + details.insertText) + ] + ); + + vscode.workspace.applyEdit(edit); + + return EditorOperationResponse.Completed; + } + + private getEditorContext(): EditorContext { + return { + currentFilePath: vscode.window.activeTextEditor.document.fileName, + cursorPosition: asPosition(vscode.window.activeTextEditor.selection.active), + selectionRange: + asRange( + new vscode.Range( + vscode.window.activeTextEditor.selection.start, + vscode.window.activeTextEditor.selection.end)) + } + } + + private openFile(filePath: string): Thenable { + + // Make sure the file path is absolute + if (!path.win32.isAbsolute(filePath)) + { + filePath = path.win32.resolve( + vscode.workspace.rootPath, + filePath); + } + + var promise = + vscode.workspace.openTextDocument(filePath) + .then(doc => vscode.window.showTextDocument(doc)) + .then(_ => EditorOperationResponse.Completed); + + return promise; + } + + private setSelection(details: SetSelectionRequestArguments): EditorOperationResponse { + vscode.window.activeTextEditor.selections = [ + new vscode.Selection( + asCodePosition(details.selectionRange.start), + asCodePosition(details.selectionRange.end)) + ] + + return EditorOperationResponse.Completed; + } } diff --git a/src/features/OpenInISE.ts b/src/features/OpenInISE.ts index 3c86547745..6c7c36edcb 100644 --- a/src/features/OpenInISE.ts +++ b/src/features/OpenInISE.ts @@ -1,20 +1,37 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + import vscode = require('vscode'); import Window = vscode.window; import ChildProcess = require('child_process'); +import { IFeature, LanguageClient } from '../feature'; + +export class OpenInISEFeature implements IFeature { + private command: vscode.Disposable; + + constructor() { + this.command = vscode.commands.registerCommand('PowerShell.OpenInISE', () => { + + var editor = Window.activeTextEditor; + var document = editor.document; + var uri = document.uri -export function registerOpenInISECommand(): void { - var disposable = vscode.commands.registerCommand('PowerShell.OpenInISE', () => { + if (process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432')) { + var ISEPath = process.env.windir + '\\Sysnative\\WindowsPowerShell\\v1.0\\powershell_ise.exe'; + } else { + var ISEPath = process.env.windir + '\\System32\\WindowsPowerShell\\v1.0\\powershell_ise.exe'; + } - var editor = Window.activeTextEditor; - var document = editor.document; - var uri = document.uri + ChildProcess.exec(ISEPath + ' -File "' + uri.fsPath + '"').unref(); + }); + } - if (process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432')) { - var ISEPath = process.env.windir + '\\Sysnative\\WindowsPowerShell\\v1.0\\powershell_ise.exe'; - } else { - var ISEPath = process.env.windir + '\\System32\\WindowsPowerShell\\v1.0\\powershell_ise.exe'; - } + public setLanguageClient(languageClient: LanguageClient) { + // Not needed for this feature. + } - ChildProcess.exec(ISEPath + ' -File "' + uri.fsPath + '"').unref(); - }); + public dispose() { + this.command.dispose(); + } } \ No newline at end of file diff --git a/src/features/PowerShellFindModule.ts b/src/features/PowerShellFindModule.ts index 923016aaf8..831ad8df1b 100644 --- a/src/features/PowerShellFindModule.ts +++ b/src/features/PowerShellFindModule.ts @@ -1,7 +1,12 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + import vscode = require('vscode'); -import { LanguageClient, RequestType, NotificationType } from 'vscode-languageclient'; import Window = vscode.window; +import { IFeature } from '../feature'; import QuickPickItem = vscode.QuickPickItem; +import { LanguageClient, RequestType, NotificationType } from 'vscode-languageclient'; export namespace FindModuleRequest { export const type: RequestType = { get method() { return 'powerShell/findModule'; } }; @@ -11,41 +16,55 @@ export namespace InstallModuleRequest { export const type: RequestType = { get method() { return 'powerShell/installModule'; } }; } -function GetCurrentTime() { +export class FindModuleFeature implements IFeature { - var timeNow = new Date(); - var hours = timeNow.getHours(); - var minutes = timeNow.getMinutes(); - var seconds = timeNow.getSeconds(); + private command: vscode.Disposable; + private languageClient: LanguageClient; - var timeString = "" + ((hours > 12) ? hours - 12 : hours); - timeString += ((minutes < 10) ? ":0" : ":") + minutes; - timeString += ((seconds < 10) ? ":0" : ":") + seconds; - timeString += (hours >= 12) ? " PM" : " AM"; + constructor() { + this.command = vscode.commands.registerCommand('PowerShell.PowerShellFindModule', () => { + var items: QuickPickItem[] = []; - return timeString; -} + vscode.window.setStatusBarMessage(this.getCurrentTime() + " Initializing..."); + this.languageClient.sendRequest(FindModuleRequest.type, null).then((modules) => { + for(var item in modules) { + items.push({ label: modules[item].name, description: modules[item].description }); + }; + + vscode.window.setStatusBarMessage(""); + Window.showQuickPick(items,{placeHolder: "Results: (" + modules.length + ")"}).then((selection) => { + if (!selection) { return; } + switch (selection.label) { + default : + var moduleName = selection.label; + //vscode.window.setStatusBarMessage("Installing PowerShell Module " + moduleName, 1500); + this.languageClient.sendRequest(InstallModuleRequest.type, moduleName); + } + }); + }); + }); + } + + public setLanguageClient(languageclient: LanguageClient) { + this.languageClient = languageclient; + } + + public dispose() { + this.command.dispose(); + } + + private getCurrentTime() { + + var timeNow = new Date(); + var hours = timeNow.getHours(); + var minutes = timeNow.getMinutes(); + var seconds = timeNow.getSeconds(); + + var timeString = "" + ((hours > 12) ? hours - 12 : hours); + timeString += ((minutes < 10) ? ":0" : ":") + minutes; + timeString += ((seconds < 10) ? ":0" : ":") + seconds; + timeString += (hours >= 12) ? " PM" : " AM"; -export function registerPowerShellFindModuleCommand(client: LanguageClient): void { - var disposable = vscode.commands.registerCommand('PowerShell.PowerShellFindModule', () => { - var items: QuickPickItem[] = []; - - vscode.window.setStatusBarMessage(GetCurrentTime() + " Initializing..."); - client.sendRequest(FindModuleRequest.type, null).then((modules) => { - for(var item in modules) { - items.push({ label: modules[item].name, description: modules[item].description }); - }; - - vscode.window.setStatusBarMessage(""); - Window.showQuickPick(items,{placeHolder: "Results: (" + modules.length + ")"}).then((selection) => { - if (!selection) { return; } - switch (selection.label) { - default : - var moduleName = selection.label; - //vscode.window.setStatusBarMessage("Installing PowerShell Module " + moduleName, 1500); - client.sendRequest(InstallModuleRequest.type, moduleName); - } - }); - }); - }); + return timeString; + } } \ No newline at end of file diff --git a/src/features/ShowOnlineHelp.ts b/src/features/ShowOnlineHelp.ts index 794fc3478d..54e38d2072 100644 --- a/src/features/ShowOnlineHelp.ts +++ b/src/features/ShowOnlineHelp.ts @@ -1,20 +1,43 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + import vscode = require('vscode'); +import { IFeature } from '../feature'; import { LanguageClient, RequestType, NotificationType } from 'vscode-languageclient'; export namespace ShowOnlineHelpRequest { export const type: RequestType = { get method() { return 'powerShell/showOnlineHelp'; } }; } -export function registerShowHelpCommand(client: LanguageClient): void { - var disposable = vscode.commands.registerCommand('PowerShell.OnlineHelp', () => { +export class ShowHelpFeature implements IFeature { + + private command: vscode.Disposable; + private languageClient: LanguageClient; + + constructor() { + this.command = vscode.commands.registerCommand('PowerShell.OnlineHelp', () => { + if (this.languageClient === undefined) { + // TODO: Log error message + return; + } - const editor = vscode.window.activeTextEditor; + const editor = vscode.window.activeTextEditor; - var selection = editor.selection; - var doc = editor.document; - var cwr = doc.getWordRangeAtPosition(selection.active) - var text = doc.getText(cwr); + var selection = editor.selection; + var doc = editor.document; + var cwr = doc.getWordRangeAtPosition(selection.active) + var text = doc.getText(cwr); - client.sendRequest(ShowOnlineHelpRequest.type, text); - }); -} \ No newline at end of file + this.languageClient.sendRequest(ShowOnlineHelpRequest.type, text); + }); + } + + public setLanguageClient(languageclient: LanguageClient) { + this.languageClient = languageclient; + } + + public dispose() { + this.command.dispose(); + } +} diff --git a/src/logging.ts b/src/logging.ts index 4e6d38e4af..fe79c3268a 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,5 +1,162 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + import fs = require('fs'); +import os = require('os'); +import path = require('path'); +import vscode = require('vscode'); +import utils = require('./utils'); +import { ILogger } from 'vscode-jsonrpc'; + +export enum LogLevel { + Verbose, + Normal, + Warning, + Error +} + +export class Logger { + + private commands: vscode.Disposable[]; + private logChannel: vscode.OutputChannel; + private logFilePath: string; + + public logBasePath: string; + public logSessionPath: string; + public MinimumLogLevel: LogLevel = LogLevel.Normal; + + constructor() { + this.logChannel = vscode.window.createOutputChannel("PowerShell Extension Logs"); + + this.logBasePath = path.resolve(__dirname, "../logs"); + utils.ensurePathExists(this.logBasePath); + + this.commands = [ + vscode.commands.registerCommand( + 'PowerShell.ShowLogs', + () => { this.showLogPanel(); }), + + vscode.commands.registerCommand( + 'PowerShell.OpenLogFolder', + () => { this.openLogFolder(); }) + ] + } + + public getLogFilePath(baseName: string): string { + return path.resolve(this.logSessionPath, `${baseName}.log`); + } + + public writeAtLevel(logLevel: LogLevel, message: string, ...additionalMessages: string[]) { + if (logLevel >= this.MinimumLogLevel) { + // TODO: Add timestamp + this.logChannel.appendLine(message); + fs.appendFile(this.logFilePath, message + os.EOL); + + additionalMessages.forEach((line) => { + this.logChannel.appendLine(line); + fs.appendFile(this.logFilePath, line + os.EOL); + }); + } + } + + public write(message: string, ...additionalMessages: string[]) { + this.writeAtLevel(LogLevel.Normal, message, ...additionalMessages); + } + + public writeVerbose(message: string, ...additionalMessages: string[]) { + this.writeAtLevel(LogLevel.Verbose, message, ...additionalMessages); + } + + public writeWarning(message: string, ...additionalMessages: string[]) { + this.writeAtLevel(LogLevel.Warning, message, ...additionalMessages); + } + + public writeAndShowWarning(message: string, ...additionalMessages: string[]) { + this.writeWarning(message, ...additionalMessages); + + vscode.window.showWarningMessage(message, "Show Logs").then((selection) => { + if (selection !== undefined) { + this.showLogPanel(); + } + }); + } + + public writeError(message: string, ...additionalMessages: string[]) { + this.writeAtLevel(LogLevel.Error, message, ...additionalMessages); + } + + public writeAndShowError(message: string, ...additionalMessages: string[]) { + this.writeError(message, ...additionalMessages); + + vscode.window.showErrorMessage(message, "Show Logs").then((selection) => { + if (selection !== undefined) { + this.showLogPanel(); + } + }); + } + + public startNewLog(minimumLogLevel: string = "Normal") { + this.MinimumLogLevel = this.logLevelNameToValue(minimumLogLevel.trim()); + + this.logSessionPath = + path.resolve( + this.logBasePath, + `${Math.floor(Date.now() / 1000)}-${vscode.env.sessionId}`); + + this.logFilePath = this.getLogFilePath("vscode-powershell"); + + utils.ensurePathExists(this.logSessionPath); + } + + private logLevelNameToValue(logLevelName: string): LogLevel { + switch (logLevelName.toLowerCase()) { + case "normal": return LogLevel.Normal; + case "verbose": return LogLevel.Verbose; + case "warning": return LogLevel.Warning; + case "error": return LogLevel.Error; + default: return LogLevel.Normal; + } + } + + public dispose() { + this.commands.forEach((command) => { command.dispose() }); + this.logChannel.dispose(); + } + + private showLogPanel() { + this.logChannel.show(); + } + + private openLogFolder() { + if (this.logSessionPath) { + // Open the folder in VS Code since there isn't an easy way to + // open the folder in the platform's file browser + vscode.commands.executeCommand( + 'vscode.openFolder', + vscode.Uri.file(this.logSessionPath), + true); + } + } +} + +export class LanguageClientLogger implements ILogger { + + constructor(private logger: Logger) { } + + public error(message: string) { + this.logger.writeError(message); + } + + public warn(message: string) { + this.logger.writeWarning(message); + } + + public info(message: string) { + this.logger.write(message); + } -export function getLogName(baseName: string): string { - return Math.floor(Date.now() / 1000) + '-' + baseName + '.log'; + public log(message: string) { + this.logger.writeVerbose(message); + } } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index cec8c959fd..526a74f057 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,39 +4,31 @@ 'use strict'; -import os = require('os'); -import fs = require('fs'); -import cp = require('child_process'); -import path = require('path'); -import utils = require('./utils'); import vscode = require('vscode'); -import logging = require('./logging'); -import settingsManager = require('./settings'); -import { StringDecoder } from 'string_decoder'; -import { LanguageClient, LanguageClientOptions, Executable, RequestType, NotificationType, StreamInfo } from 'vscode-languageclient'; -import { registerExpandAliasCommand } from './features/ExpandAlias'; -import { registerShowHelpCommand } from './features/ShowOnlineHelp'; -import { registerOpenInISECommand } from './features/OpenInISE'; -import { registerPowerShellFindModuleCommand } from './features/PowerShellFindModule'; -import { registerConsoleCommands } from './features/Console'; -import { registerExtensionCommands } from './features/ExtensionCommands'; -import net = require('net'); +import { Logger, LogLevel } from './logging'; +import { IFeature } from './feature'; +import { SessionManager } from './session'; +import { PowerShellLanguageId } from './utils'; +import { ConsoleFeature } from './features/Console'; +import { OpenInISEFeature } from './features/OpenInISE'; +import { ExpandAliasFeature } from './features/ExpandAlias'; +import { ShowHelpFeature } from './features/ShowOnlineHelp'; +import { FindModuleFeature } from './features/PowerShellFindModule'; +import { ExtensionCommandsFeature } from './features/ExtensionCommands'; // NOTE: We will need to find a better way to deal with the required // PS Editor Services version... var requiredEditorServicesVersion = "0.7.2"; -var powerShellProcess: cp.ChildProcess = undefined; -var languageServerClient: LanguageClient = undefined; -var PowerShellLanguageId = 'powershell'; -var powerShellLogWriter: fs.WriteStream = undefined; +var logger: Logger = undefined; +var sessionManager: SessionManager = undefined; +var extensionFeatures: IFeature[] = []; export function activate(context: vscode.ExtensionContext): void { - var settings = settingsManager.load('powershell'); - - vscode.languages.setLanguageConfiguration(PowerShellLanguageId, + vscode.languages.setLanguageConfiguration( + PowerShellLanguageId, { wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\=\+\[\{\]\}\\\|\;\'\"\,\.\<\>\/\?\s]+)/g, @@ -73,265 +65,37 @@ export function activate(context: vscode.ExtensionContext): void { } }); - // Get the current version of this extension - var hostVersion = - vscode - .extensions - .getExtension("ms-vscode.PowerShell") - .packageJSON - .version; - - var bundledModulesPath = settings.developer.bundledModulesPath; - if (!path.isAbsolute(bundledModulesPath)) { - bundledModulesPath = path.resolve(__dirname, bundledModulesPath); - } - - var startArgs = - '-EditorServicesVersion "' + requiredEditorServicesVersion + '" ' + - '-HostName "Visual Studio Code Host" ' + - '-HostProfileId "Microsoft.VSCode" ' + - '-HostVersion "' + hostVersion + '" ' + - '-BundledModulesPath "' + bundledModulesPath + '" '; - - if (settings.developer.editorServicesWaitForDebugger) { - startArgs += '-WaitForDebugger '; - } - if (settings.developer.editorServicesLogLevel) { - startArgs += '-LogLevel "' + settings.developer.editorServicesLogLevel + '" ' - } - - // 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 (os.platform() == "win32") { - powerShellExePath = - settings.useX86Host || !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"; - - // Check for OpenSSL dependency on OS X - if (!utils.checkIfFileExists("/usr/local/lib/libcrypto.1.0.0.dylib") || - !utils.checkIfFileExists("/usr/local/lib/libssl.1.0.0.dylib")) { - var thenable = - vscode.window.showWarningMessage( - "The PowerShell extension will not work without OpenSSL on Mac OS X", - "Show Documentation"); - - thenable.then( - (s) => { - if (s === "Show Documentation") { - cp.exec("open https://github.com/PowerShell/vscode-powershell/blob/master/docs/troubleshooting.md#1-powershell-intellisense-does-not-work-cant-debug-scripts"); - } - }); - - // Don't continue initializing since Editor Services will not load successfully - console.log("Cannot start PowerShell Editor Services due to missing OpenSSL dependency."); - return; - } - } - else { - powerShellExePath = "/usr/bin/powershell"; - } - - // Is there a setting override for the PowerShell path? - if (settings.developer.powerShellExePath && - settings.developer.powerShellExePath.trim().length > 0) { - - powerShellExePath = settings.developer.powerShellExePath; - - // If the path does not exist, show an error - fs.access( - powerShellExePath, fs.X_OK, - (err) => { - if (err) { - vscode.window.showErrorMessage( - "powershell.exe cannot be found or is not accessible at path " + powerShellExePath); - } - else { - startPowerShell( - powerShellExePath, - bundledModulesPath, - startArgs); - } - }); - } - else { - startPowerShell( - powerShellExePath, - bundledModulesPath, - startArgs); - } -} - -function startPowerShell(powerShellExePath: string, bundledModulesPath: string, startArgs: string) { - try - { - let startScriptPath = - path.resolve( - __dirname, - '../scripts/Start-EditorServices.ps1'); - - var logBasePath = path.resolve(__dirname, "../logs"); - utils.ensurePathExists(logBasePath); - - var editorServicesLogName = logging.getLogName("EditorServices"); - var powerShellLogName = logging.getLogName("PowerShell"); - - startArgs += - '-LogPath "' + path.resolve(logBasePath, editorServicesLogName) + '" '; - - let args = [ - '-NoProfile', - '-NonInteractive' - ] - - // Only add ExecutionPolicy param on Windows - if (os.platform() == "win32") { - args.push('-ExecutionPolicy'); - args.push('Unrestricted'); - } - - // Add the Start-EditorServices.ps1 invocation arguments - args.push('-Command') - args.push('& "' + startScriptPath + '" ' + startArgs) - - // Launch PowerShell as child process - powerShellProcess = cp.spawn(powerShellExePath, args); - - // Open a log file to be used for PowerShell.exe output - powerShellLogWriter = - fs.createWriteStream( - path.resolve(logBasePath, powerShellLogName)) - - var decoder = new StringDecoder('utf8'); - powerShellProcess.stdout.on( - 'data', - (data: Buffer) => { - powerShellLogWriter.write("OUTPUT: " + data); - var response = JSON.parse(decoder.write(data).trim()); - - if (response["status"] === "started") { - let sessionDetails: utils.EditorServicesSessionDetails = response; - - // Write out the session configuration file - utils.writeSessionFile(sessionDetails); - - // Start the language service client - startLanguageClient(sessionDetails.languageServicePort, powerShellLogWriter); - } - else { - // TODO: Handle other response cases - } - }); - - powerShellProcess.stderr.on( - 'data', - (data) => { - console.log("powershell.exe - ERROR: " + data); - powerShellLogWriter.write("ERROR: " + data); - }); - - powerShellProcess.on( - 'close', - (exitCode) => { - console.log("powershell.exe terminated with exit code: " + exitCode); - powerShellLogWriter.write("\r\npowershell.exe terminated with exit code: " + exitCode + "\r\n"); - - if (languageServerClient != undefined) { - languageServerClient.stop(); - } - }); - - console.log("powershell.exe started, pid: " + powerShellProcess.pid + ", exe: " + powerShellExePath); - powerShellLogWriter.write( - "powershell.exe started --" + - "\r\n pid: " + powerShellProcess.pid + - "\r\n exe: " + powerShellExePath + - "\r\n bundledModulesPath: " + bundledModulesPath + - "\r\n args: " + startScriptPath + ' ' + startArgs + "\r\n\r\n"); - - // TODO: Set timeout for response from powershell.exe - } - catch (e) - { - vscode.window.showErrorMessage( - "The language service could not be started: " + e); - } -} - -function startLanguageClient(port: number, logWriter: fs.WriteStream) { - - logWriter.write("Connecting to port: " + port + "\r\n"); - - try - { - let connectFunc = () => { - return new Promise( - (resolve, reject) => { - var socket = net.connect(port); - socket.on( - 'connect', - function() { - console.log("Socket connected!"); - resolve({writer: socket, reader: socket}) - }); - }); - }; - - let clientOptions: LanguageClientOptions = { - documentSelector: [PowerShellLanguageId], - synchronize: { - configurationSection: PowerShellLanguageId, - //fileEvents: vscode.workspace.createFileSystemWatcher('**/.eslintrc') - } - } - - languageServerClient = - new LanguageClient( - 'PowerShell Editor Services', - connectFunc, - clientOptions); - - languageServerClient.onReady().then( - () => registerFeatures(), - (reason) => vscode.window.showErrorMessage("Could not start language service: " + reason)); - - languageServerClient.start(); - } - catch (e) - { - vscode.window.showErrorMessage( - "The language service could not be started: " + e); - } -} - -function registerFeatures() { - // Register other features - registerExpandAliasCommand(languageServerClient); - registerShowHelpCommand(languageServerClient); - registerConsoleCommands(languageServerClient); - registerOpenInISECommand(); - registerPowerShellFindModuleCommand(languageServerClient); - registerExtensionCommands(languageServerClient); + // Create the logger + logger = new Logger(); + + // Create features + extensionFeatures = [ + new ConsoleFeature(), + new OpenInISEFeature(), + new ExpandAliasFeature(), + new ShowHelpFeature(), + new FindModuleFeature(), + new ExtensionCommandsFeature() + ]; + + sessionManager = + new SessionManager( + requiredEditorServicesVersion, + logger, + extensionFeatures); + + sessionManager.start(); } export function deactivate(): void { - powerShellLogWriter.write("\r\n\r\nShutting down language client..."); - - // Close the language server client - if (languageServerClient) { - languageServerClient.stop(); - languageServerClient = undefined; - } + // Finish the logger + logger.dispose(); - // Clean up the session file - utils.deleteSessionFile(); + // Clean up all extension features + extensionFeatures.forEach(feature => { + feature.dispose(); + }); - // Kill the PowerShell process we spawned - powerShellLogWriter.write("\r\nTerminating PowerShell process..."); - powerShellProcess.kill(); + // Dispose of the current session + sessionManager.dispose(); } diff --git a/src/session.ts b/src/session.ts new file mode 100644 index 0000000000..66877cf4af --- /dev/null +++ b/src/session.ts @@ -0,0 +1,582 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import os = require('os'); +import fs = require('fs'); +import net = require('net'); +import path = require('path'); +import utils = require('./utils'); +import vscode = require('vscode'); +import cp = require('child_process'); +import Settings = require('./settings'); + +import { Logger } from './logging'; +import { IFeature } from './feature'; +import { StringDecoder } from 'string_decoder'; +import { LanguageClient, LanguageClientOptions, Executable, RequestType, NotificationType, StreamInfo } from 'vscode-languageclient'; + +export enum SessionStatus { + NotStarted, + Initializing, + Running, + Stopping, + Failed +} + +enum SessionType { + UseDefault, + UseCurrent, + UsePath, + UseBuiltIn +} + +interface DefaultSessionConfiguration { + type: SessionType.UseDefault +} + +interface CurrentSessionConfiguration { + type: SessionType.UseCurrent, +} + +interface PathSessionConfiguration { + type: SessionType.UsePath, + path: string; +} + +interface BuiltInSessionConfiguration { + type: SessionType.UseBuiltIn; + path?: string; + is32Bit: boolean; +} + +type SessionConfiguration = + DefaultSessionConfiguration | + CurrentSessionConfiguration | + PathSessionConfiguration | + BuiltInSessionConfiguration; + +export class SessionManager { + + private ShowSessionMenuCommandName = "PowerShell.ShowSessionMenu"; + + private hostVersion: string; + private isWindowsOS: boolean; + private sessionStatus: SessionStatus; + private powerShellProcess: cp.ChildProcess; + private statusBarItem: vscode.StatusBarItem; + private sessionConfiguration: SessionConfiguration; + private versionDetails: PowerShellVersionDetails; + private registeredCommands: vscode.Disposable[] = []; + private languageServerClient: LanguageClient = undefined; + private sessionSettings: Settings.ISettings = undefined; + + constructor( + private requiredEditorServicesVersion: string, + private log: Logger, + private extensionFeatures: IFeature[] = []) { + + this.isWindowsOS = os.platform() == "win32"; + + // Get the current version of this extension + this.hostVersion = + vscode + .extensions + .getExtension("ms-vscode.PowerShell") + .packageJSON + .version; + + this.registerCommands(); + this.createStatusBarItem(); + } + + public start(sessionConfig: SessionConfiguration = { type: SessionType.UseDefault }) { + this.sessionSettings = Settings.load(utils.PowerShellLanguageId); + this.log.startNewLog(this.sessionSettings.developer.editorServicesLogLevel); + + this.sessionConfiguration = this.resolveSessionConfiguration(sessionConfig); + + if (this.sessionConfiguration.type === SessionType.UsePath || + this.sessionConfiguration.type === SessionType.UseBuiltIn) { + + var bundledModulesPath = this.sessionSettings.developer.bundledModulesPath; + if (!path.isAbsolute(bundledModulesPath)) { + bundledModulesPath = path.resolve(__dirname, bundledModulesPath); + } + + var startArgs = + "-EditorServicesVersion '" + this.requiredEditorServicesVersion + "' " + + "-HostName 'Visual Studio Code Host' " + + "-HostProfileId 'Microsoft.VSCode' " + + "-HostVersion '" + this.hostVersion + "' " + + "-BundledModulesPath '" + bundledModulesPath + "' "; + + if (this.sessionSettings.developer.editorServicesWaitForDebugger) { + startArgs += '-WaitForDebugger '; + } + if (this.sessionSettings.developer.editorServicesLogLevel) { + startArgs += "-LogLevel '" + this.sessionSettings.developer.editorServicesLogLevel + "' " + } + + this.startPowerShell( + this.sessionConfiguration.path, + bundledModulesPath, + startArgs); + } + else { + this.setSessionFailure("PowerShell could not be started, click 'Show Logs' for more details."); + } + } + + public stop() { + + // Shut down existing session if there is one + this.log.write(os.EOL + os.EOL + "Shutting down language client..."); + + if (this.sessionStatus === SessionStatus.Failed) { + // Before moving further, clear out the client and process if + // the process is already dead (i.e. it crashed) + this.languageServerClient = undefined; + this.powerShellProcess = undefined; + } + + this.sessionStatus = SessionStatus.Stopping; + + // Close the language server client + if (this.languageServerClient !== undefined) { + this.languageServerClient.stop(); + this.languageServerClient = undefined; + } + + // Clean up the session file + utils.deleteSessionFile(); + + // Kill the PowerShell process we spawned via the console + if (this.powerShellProcess !== undefined) { + this.log.write(os.EOL + "Terminating PowerShell process..."); + this.powerShellProcess.kill(); + this.powerShellProcess = undefined; + } + + this.sessionStatus = SessionStatus.NotStarted; + } + + public dispose() : void { + // Stop the current session + this.stop(); + + // Dispose of all commands + this.registeredCommands.forEach(command => { command.dispose(); }); + } + + private onConfigurationUpdated() { + var settings = Settings.load(utils.PowerShellLanguageId); + + // Detect any setting changes that would affect the session + if (settings.useX86Host !== this.sessionSettings.useX86Host || + settings.developer.powerShellExePath.toLowerCase() !== this.sessionSettings.developer.powerShellExePath.toLowerCase() || + settings.developer.editorServicesLogLevel.toLowerCase() !== this.sessionSettings.developer.editorServicesLogLevel.toLowerCase() || + settings.developer.bundledModulesPath.toLowerCase() !== this.sessionSettings.developer.bundledModulesPath.toLowerCase()) { + + vscode.window.showInformationMessage( + "The PowerShell runtime configuration has changed, would you like to start a new session?", + "Yes", "No") + .then((response) => { + if (response === "Yes") { + this.restartSession({ type: SessionType.UseDefault }) + } + }); + } + } + + private registerCommands() : void { + this.registeredCommands = [ + vscode.commands.registerCommand('PowerShell.RestartSession', () => { this.restartSession(); }), + vscode.commands.registerCommand(this.ShowSessionMenuCommandName, () => { this.showSessionMenu(); }), + vscode.workspace.onDidChangeConfiguration(() => this.onConfigurationUpdated()) + ] + } + + private startPowerShell(powerShellExePath: string, bundledModulesPath: string, startArgs: string) { + try + { + this.setSessionStatus( + "Starting PowerShell...", + SessionStatus.Initializing); + + let startScriptPath = + path.resolve( + __dirname, + '../scripts/Start-EditorServices.ps1'); + + var editorServicesLogPath = this.log.getLogFilePath("EditorServices"); + + startArgs += "-LogPath '" + editorServicesLogPath + "' "; + + var powerShellArgs = [ + "-NoProfile", + "-NonInteractive" + ] + + // Only add ExecutionPolicy param on Windows + if (this.isWindowsOS) { + powerShellArgs.push("-ExecutionPolicy", "Unrestricted") + } + + powerShellArgs.push( + "-Command", + "& '" + startScriptPath + "' " + startArgs) + + // Launch PowerShell as child process + this.powerShellProcess = cp.spawn(powerShellExePath, powerShellArgs); + + var decoder = new StringDecoder('utf8'); + this.powerShellProcess.stdout.on( + 'data', + (data: Buffer) => { + this.log.write("OUTPUT: " + data); + var response = JSON.parse(decoder.write(data).trim()); + + if (response["status"] === "started") { + let sessionDetails: utils.EditorServicesSessionDetails = response; + + // Write out the session configuration file + utils.writeSessionFile(sessionDetails); + + // Start the language service client + this.startLanguageClient(sessionDetails.languageServicePort); + } + else { + // TODO: Handle other response cases + } + }); + + this.powerShellProcess.stderr.on( + 'data', + (data) => { + this.log.writeError("ERROR: " + data); + + if (this.sessionStatus === SessionStatus.Initializing) { + this.setSessionFailure("PowerShell could not be started, click 'Show Logs' for more details."); + } + else if (this.sessionStatus === SessionStatus.Running) { + this.promptForRestart(); + } + }); + + this.powerShellProcess.on( + 'close', + (exitCode) => { + this.log.write(os.EOL + "powershell.exe terminated with exit code: " + exitCode + os.EOL); + + if (this.languageServerClient != undefined) { + this.languageServerClient.stop(); + } + + if (this.sessionStatus === SessionStatus.Running) { + this.setSessionStatus("Session exited", SessionStatus.Failed); + this.promptForRestart(); + } + }); + + console.log("powershell.exe started, pid: " + this.powerShellProcess.pid + ", exe: " + powerShellExePath); + this.log.write( + "powershell.exe started --", + " pid: " + this.powerShellProcess.pid, + " exe: " + powerShellExePath, + " bundledModulesPath: " + bundledModulesPath, + " args: " + startScriptPath + ' ' + startArgs + os.EOL + os.EOL); + } + catch (e) + { + this.setSessionFailure("The language service could not be started: ", e); + } + } + + private promptForRestart() { + vscode.window.showErrorMessage( + "The PowerShell session has terminated due to an error, would you like to restart it?", + "Yes", "No") + .then((answer) => { if (answer === "Yes") { this.restartSession(); }}); + } + + private startLanguageClient(port: number) { + + this.log.write("Connecting to language service on port " + port + "..." + os.EOL); + + try + { + let connectFunc = () => { + return new Promise( + (resolve, reject) => { + var socket = net.connect(port); + socket.on( + 'connect', + () => { + this.log.write("Language service connected."); + resolve({writer: socket, reader: socket}) + }); + }); + }; + + let clientOptions: LanguageClientOptions = { + documentSelector: [utils.PowerShellLanguageId], + synchronize: { + configurationSection: utils.PowerShellLanguageId, + //fileEvents: vscode.workspace.createFileSystemWatcher('**/.eslintrc') + } + } + + this.languageServerClient = + new LanguageClient( + 'PowerShell Editor Services', + connectFunc, + clientOptions); + + this.languageServerClient.onReady().then( + () => { + this.languageServerClient + .sendRequest(PowerShellVersionRequest.type) + .then( + (versionDetails) => { + this.versionDetails = versionDetails; + this.setSessionStatus( + this.versionDetails.architecture === "x86" + ? `${this.versionDetails.displayVersion} (${this.versionDetails.architecture})` + : this.versionDetails.displayVersion, + SessionStatus.Running); + + this.updateExtensionFeatures(this.languageServerClient) + }); + }, + (reason) => { + this.setSessionFailure("Could not start language service: ", reason); + }); + + this.languageServerClient.start(); + } + catch (e) + { + this.setSessionFailure("The language service could not be started: ", e); + } + } + + private updateExtensionFeatures(languageClient: LanguageClient) { + this.extensionFeatures.forEach(feature => { + feature.setLanguageClient(languageClient); + }); + } + + private restartSession(sessionConfig?: SessionConfiguration) { + this.stop(); + this.start(sessionConfig); + } + + private createStatusBarItem() { + if (this.statusBarItem == undefined) { + // Create the status bar item and place it right next + // to the language indicator + this.statusBarItem = + vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Right, + 1); + + this.statusBarItem.command = this.ShowSessionMenuCommandName; + this.statusBarItem.show(); + } + } + + private setSessionStatus(statusText: string, status: SessionStatus): void { + // Set color and icon for 'Running' by default + var statusIconText = "$(terminal) "; + var statusColor = "#affc74"; + + if (status == SessionStatus.Initializing) { + statusIconText = "$(sync) "; + statusColor = "#f3fc74"; + } + else if (status == SessionStatus.Failed) { + statusIconText = "$(alert) "; + statusColor = "#fcc174"; + } + + this.sessionStatus = status; + this.statusBarItem.color = statusColor; + this.statusBarItem.text = statusIconText + statusText; + } + + private setSessionFailure(message: string, ...additionalMessages: string[]) { + this.log.writeAndShowError(message, ...additionalMessages); + + this.setSessionStatus( + "Initialization Error", + SessionStatus.Failed); + } + + private resolveSessionConfiguration(sessionConfig: SessionConfiguration): SessionConfiguration { + + switch (sessionConfig.type) { + case SessionType.UseCurrent: return this.sessionConfiguration; + case SessionType.UseDefault: + // Is there a setting override for the PowerShell path? + var powerShellExePath = (this.sessionSettings.developer.powerShellExePath || "").trim(); + if (powerShellExePath.length > 0) { + return this.resolveSessionConfiguration( + { type: SessionType.UsePath, path: this.sessionSettings.developer.powerShellExePath}); + } + else { + return this.resolveSessionConfiguration( + { type: SessionType.UseBuiltIn, is32Bit: this.sessionSettings.useX86Host }); + } + + case SessionType.UsePath: + sessionConfig.path = this.resolvePowerShellPath(sessionConfig.path); + return sessionConfig; + + case SessionType.UseBuiltIn: + sessionConfig.path = this.getBuiltInPowerShellPath(sessionConfig.is32Bit); + return sessionConfig; + } + } + + private getBuiltInPowerShellPath(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"; + + // Check for OpenSSL dependency on OS X + if (!utils.checkIfFileExists("/usr/local/lib/libcrypto.1.0.0.dylib") || + !utils.checkIfFileExists("/usr/local/lib/libssl.1.0.0.dylib")) { + var thenable = + vscode.window.showWarningMessage( + "The PowerShell extension will not work without OpenSSL on Mac OS X", + "Show Documentation"); + + thenable.then( + (s) => { + if (s === "Show Documentation") { + cp.exec("open https://github.com/PowerShell/vscode-powershell/blob/master/docs/troubleshooting.md#1-powershell-intellisense-does-not-work-cant-debug-scripts"); + } + }); + + // Don't continue initializing since Editor Services will not load successfully + this.setSessionFailure("Cannot start PowerShell Editor Services due to missing OpenSSL dependency."); + return null; + } + } + else { + powerShellExePath = "/usr/bin/powershell"; + } + + return this.resolvePowerShellPath(powerShellExePath); + } + + private resolvePowerShellPath(powerShellExePath: string): string { + // If the path does not exist, show an error + if (!utils.checkIfFileExists(powerShellExePath)) { + this.setSessionFailure( + "powershell.exe cannot be found or is not accessible at path " + powerShellExePath); + + return null; + } + + return powerShellExePath; + } + + private showSessionMenu() { + var menuItems: SessionMenuItem[] = [ + new SessionMenuItem( + `Current session: PowerShell ${this.versionDetails.displayVersion} (${this.versionDetails.architecture}) ${this.versionDetails.edition} Edition [${this.versionDetails.version}]`, + () => { vscode.commands.executeCommand("PowerShell.ShowLogs"); }), + + new SessionMenuItem( + "Restart Current Session", + () => { this.restartSession(); }), + ]; + + if (this.isWindowsOS) { + var item32 = + new SessionMenuItem( + "Switch to Windows PowerShell (x86)", + () => { this.restartSession({ type: SessionType.UseBuiltIn, is32Bit: true}) }); + + var item64 = + new SessionMenuItem( + "Switch to Windows PowerShell (x64)", + () => { this.restartSession({ type: SessionType.UseBuiltIn, is32Bit: false }) }); + + // If the configured PowerShell path isn't being used, offer it as an option + if (this.sessionSettings.developer.powerShellExePath !== "" && + (this.sessionConfiguration.type !== SessionType.UsePath || + this.sessionConfiguration.path !== this.sessionSettings.developer.powerShellExePath)) { + + menuItems.push( + new SessionMenuItem( + `Switch to PowerShell at path: ${this.sessionSettings.developer.powerShellExePath}`, + () => { + this.restartSession( + { type: SessionType.UsePath, path: this.sessionSettings.developer.powerShellExePath }) + })); + } + + if (this.sessionConfiguration.type === SessionType.UseBuiltIn) { + menuItems.push( + this.sessionConfiguration.is32Bit ? item64 : item32); + } + else { + menuItems.push(item32); + menuItems.push(item64); + } + } + else { + if (this.sessionConfiguration.type !== SessionType.UseBuiltIn) { + menuItems.push( + new SessionMenuItem( + "Use built-in PowerShell", + () => { this.restartSession({ type: SessionType.UseBuiltIn, is32Bit: false }) })); + } + } + + menuItems.push( + new SessionMenuItem( + "Open Session Logs Folder", + () => { vscode.commands.executeCommand("PowerShell.OpenLogFolder"); })); + + vscode + .window + .showQuickPick(menuItems) + .then((selectedItem) => { selectedItem.callback(); }); + } +} + +class SessionMenuItem implements vscode.QuickPickItem { + public description: string; + + constructor( + public readonly label: string, + public readonly callback: () => void = () => { }) + { + } +} + +export namespace PowerShellVersionRequest { + export const type: RequestType = + { get method() { return 'powerShell/getVersion'; } }; +} + +export interface PowerShellVersionDetails { + version: string; + displayVersion: string; + edition: string; + architecture: string; +} diff --git a/src/utils.ts b/src/utils.ts index 6694f5eafa..15f489f7e9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,6 +2,8 @@ import fs = require('fs'); import os = require('os'); import path = require('path'); +export let PowerShellLanguageId = 'powershell'; + export function ensurePathExists(targetPath: string) { // Ensure that the path exists try { @@ -57,13 +59,21 @@ export interface EditorServicesSessionDetails { languageServicePort: number; debugServicePort: number; } + export interface ReadSessionFileCallback { (details: EditorServicesSessionDetails): void; } -let sessionsFolder = path.resolve(__dirname, "sessions/"); +let sessionsFolder = path.resolve(__dirname, "..", "sessions/"); let sessionFilePath = path.resolve(sessionsFolder, "PSES-VSCode-" + process.env.VSCODE_PID); +// Create the sessions path if it doesn't exist already +ensurePathExists(sessionsFolder); + +export function getSessionFilePath() { + return sessionFilePath; +} + export function writeSessionFile(sessionDetails: EditorServicesSessionDetails) { ensurePathExists(sessionsFolder); @@ -78,12 +88,17 @@ export function readSessionFile(): EditorServicesSessionDetails { } export function deleteSessionFile() { - fs.unlinkSync(sessionFilePath); + try { + fs.unlinkSync(sessionFilePath); + } + catch (e) { + // TODO: Be more specific about what we're catching + } } export function checkIfFileExists(filePath: string): boolean { try { - fs.accessSync(filePath, fs.R_OK) + fs.accessSync(filePath, fs.constants.R_OK) return true; } catch (e) { diff --git a/tsconfig.json b/tsconfig.json index 248f5a0ba2..bc0dac725d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { "module": "commonjs", + "target": "es6", "outDir": "out", - "noLib": true, - "target": "es5", + "lib": ["es6"], "sourceMap": true }, "exclude": [ diff --git a/typings/vscode-typings.d.ts b/typings/vscode-typings.d.ts deleted file mode 100644 index b9b54de1eb..0000000000 --- a/typings/vscode-typings.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file