diff --git a/examples/.vscode/launch.json b/examples/.vscode/launch.json index 846f6ab009..88687248dd 100644 --- a/examples/.vscode/launch.json +++ b/examples/.vscode/launch.json @@ -9,6 +9,15 @@ "args": [], "cwd": "${file}" }, + { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Launch Current File in Temporary Console", + "script": "${file}", + "args": [], + "cwd": "${file}", + "createTemporaryIntegratedConsole": true + }, { "type": "PowerShell", "request": "launch", diff --git a/package.json b/package.json index 55aed8660e..cd8fad0125 100644 --- a/package.json +++ b/package.json @@ -211,6 +211,19 @@ "cwd": "^\"\\${file}\"" } }, + { + "label": "PowerShell: Launch Current File in Temporary Console", + "description": "Launch current file (in active editor window) under debugger in a temporary Integrated Console.", + "body": { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Launch Current File in Temporary Console", + "script": "^\"\\${file}\"", + "args": [], + "cwd": "^\"\\${file}\"", + "createTemporaryIntegratedConsole": true + } + }, { "label": "PowerShell: Launch - Current File w/Args Prompt", "description": "Launch current file (in active editor window) under debugger, prompting first for script arguments", @@ -292,6 +305,11 @@ "type": "string", "description": "Absolute path to the working directory. Default is the current workspace.", "default": "${workspaceRoot}" + }, + "createTemporaryIntegratedConsole": { + "type": "boolean", + "description": "Determines whether a temporary PowerShell Integrated Console is created for each debugging session, useful for debugging PowerShell classes and binary modules. Overrides the user setting 'powershell.debugging.createTemporaryIntegratedConsole'.", + "default": false } } }, @@ -323,6 +341,15 @@ "args": [], "cwd": "${file}" }, + { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Launch Current File in Temporary Console", + "script": "${file}", + "args": [], + "cwd": "${file}", + "createTemporaryIntegratedConsole": true + }, { "type": "PowerShell", "request": "launch", @@ -448,6 +475,11 @@ "default": true, "description": "Switches focus to the console when a script selection is run or a script file is debugged. This is an accessibility feature. To disable it, set to false." }, + "powershell.debugging.createTemporaryIntegratedConsole": { + "type": "boolean", + "default": false, + "description": "Determines whether a temporary PowerShell Integrated Console is created for each debugging session, useful for debugging PowerShell classes and binary modules." + }, "powershell.developer.bundledModulesPath": { "type": "string", "description": "Specifies an alternate path to the folder containing modules that are bundled with the PowerShell extension (i.e. PowerShell Editor Services, PSScriptAnalyzer, Plaster)" diff --git a/scripts/Start-EditorServices.ps1 b/scripts/Start-EditorServices.ps1 index 6368c9dc51..2f3d074307 100644 --- a/scripts/Start-EditorServices.ps1 +++ b/scripts/Start-EditorServices.ps1 @@ -55,7 +55,7 @@ param( [switch] $EnableConsoleRepl, - [string] + [switch] $DebugServiceOnly, [string[]] diff --git a/src/features/DebugSession.ts b/src/features/DebugSession.ts index 75b65bf12a..98a89c0808 100644 --- a/src/features/DebugSession.ts +++ b/src/features/DebugSession.ts @@ -4,6 +4,7 @@ import vscode = require('vscode'); import utils = require('../utils'); +import Settings = require('../settings'); import { IFeature } from '../feature'; import { SessionManager } from '../session'; import { LanguageClient, RequestType, NotificationType } from 'vscode-languageclient'; @@ -13,6 +14,8 @@ export namespace StartDebuggerNotification { } export class DebugSessionFeature implements IFeature { + + private sessionCount: number = 1; private command: vscode.Disposable; private examplesPath: string; @@ -42,6 +45,9 @@ export class DebugSessionFeature implements IFeature { let debugCurrentScript = (config.script === "${file}") || !config.request; let generateLaunchConfig = !config.request; + var settings = Settings.load(); + let createNewIntegratedConsole = settings.debugging.createTemporaryIntegratedConsole; + if (generateLaunchConfig) { // No launch.json, create the default configuration for both unsaved (Untitled) and saved documents. config.type = 'PowerShell'; @@ -106,20 +112,50 @@ export class DebugSessionFeature implements IFeature { } } } + + if (config.createTemporaryIntegratedConsole !== undefined) { + createNewIntegratedConsole = config.createTemporaryIntegratedConsole; + } } // Prevent the Debug Console from opening config.internalConsoleOptions = "neverOpen"; // Create or show the interactive console - // TODO #367: Check if "newSession" mode is configured vscode.commands.executeCommand('PowerShell.ShowSessionConsole', true); - // Write out temporary debug session file - utils.writeSessionFile( - utils.getDebugSessionFilePath(), - this.sessionManager.getSessionDetails()); + var sessionFilePath = utils.getDebugSessionFilePath(); + + if (createNewIntegratedConsole) { + var debugProcess = + this.sessionManager.createDebugSessionProcess( + sessionFilePath, + settings); + + debugProcess + .start(`DebugSession-${this.sessionCount++}`) + .then( + sessionDetails => { + this.startDebugger( + config, + sessionFilePath, + sessionDetails); + }); + } + else { + this.startDebugger( + config, + sessionFilePath, + this.sessionManager.getSessionDetails()); + } + } + + private startDebugger( + config: any, + sessionFilePath: string, + sessionDetails: utils.EditorServicesSessionDetails) { + utils.writeSessionFile(sessionFilePath, sessionDetails); vscode.commands.executeCommand('vscode.startDebug', config); } } diff --git a/src/process.ts b/src/process.ts new file mode 100644 index 0000000000..ae39bb9627 --- /dev/null +++ b/src/process.ts @@ -0,0 +1,181 @@ +/*--------------------------------------------------------- + * 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'; + +export class PowerShellProcess { + + private consoleTerminal: vscode.Terminal = undefined; + private consoleCloseSubscription: vscode.Disposable; + private sessionDetails: utils.EditorServicesSessionDetails; + + private onExitedEmitter = new vscode.EventEmitter(); + public onExited: vscode.Event = this.onExitedEmitter.event; + + constructor( + public exePath: string, + private title: string, + private log: Logger, + private startArgs: string, + private sessionFilePath: string, + private sessionSettings: Settings.ISettings) { + } + + public start(logFileName: string): Thenable { + + return new Promise( + (resolve, reject) => { + try + { + let startScriptPath = + path.resolve( + __dirname, + '../scripts/Start-EditorServices.ps1'); + + var editorServicesLogPath = this.log.getLogFilePath(logFileName); + + var featureFlags = + this.sessionSettings.developer.featureFlags !== undefined + ? this.sessionSettings.developer.featureFlags.map(f => `'${f}'`).join(', ') + : ""; + + this.startArgs += + `-LogPath '${editorServicesLogPath}' ` + + `-SessionDetailsPath '${this.sessionFilePath}' ` + + `-FeatureFlags @(${featureFlags})` + + var powerShellArgs = [ + "-NoProfile", + "-NonInteractive" + ] + + // Only add ExecutionPolicy param on Windows + if (utils.isWindowsOS()) { + powerShellArgs.push("-ExecutionPolicy", "Unrestricted") + } + + powerShellArgs.push( + "-Command", + "& '" + startScriptPath + "' " + this.startArgs); + + var powerShellExePath = this.exePath; + + if (this.sessionSettings.developer.powerShellExeIsWindowsDevBuild) { + // Windows PowerShell development builds need the DEVPATH environment + // variable set to the folder where development binaries are held + + // 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'); + fs.writeFileSync( + batScriptPath, + `@set DEVPATH=${path.dirname(powerShellExePath)}\r\n@${powerShellExePath} %*`); + + powerShellExePath = batScriptPath; + } + + this.log.write(`${utils.getTimestampString()} Language server starting...`); + + // Make sure no old session file exists + utils.deleteSessionFile(this.sessionFilePath); + + // Launch PowerShell in the integrated terminal + this.consoleTerminal = + vscode.window.createTerminal( + this.title, + powerShellExePath, + powerShellArgs); + + if (this.sessionSettings.integratedConsole.showOnStartup) { + this.consoleTerminal.show(true); + } + + // Start the language client + utils.waitForSessionFile( + this.sessionFilePath, + (sessionDetails, error) => { + // Clean up the session file + utils.deleteSessionFile(this.sessionFilePath); + + if (error) { + reject(error); + } + else { + this.sessionDetails = sessionDetails; + resolve(this.sessionDetails); + } + }); + + // 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.consoleCloseSubscription = + vscode.window.onDidCloseTerminal( + terminal => { + if (terminal === this.consoleTerminal) { + this.log.write(os.EOL + "powershell.exe terminated or terminal UI was closed" + os.EOL); + this.onExitedEmitter.fire(); + } + }); + + this.consoleTerminal.processId.then( + pid => { + console.log("powershell.exe started, pid: " + pid + ", exe: " + powerShellExePath); + this.log.write( + "powershell.exe started --", + " pid: " + pid, + " exe: " + powerShellExePath, + " args: " + startScriptPath + ' ' + this.startArgs + os.EOL + os.EOL); + }); + } + catch (e) + { + reject(e); + } + }); + } + + public showConsole(preserveFocus: boolean) { + if (this.consoleTerminal) { + this.consoleTerminal.show(preserveFocus); + } + } + + public dispose() { + + // Clean up the session file + utils.deleteSessionFile(this.sessionFilePath); + + if (this.consoleCloseSubscription) { + this.consoleCloseSubscription.dispose(); + this.consoleCloseSubscription = undefined; + } + + if (this.consoleTerminal) { + this.log.write(os.EOL + "Terminating PowerShell process..."); + this.consoleTerminal.dispose(); + this.consoleTerminal = undefined; + } + } +} diff --git a/src/session.ts b/src/session.ts index 76a13b35cc..f6fde9de1f 100644 --- a/src/session.ts +++ b/src/session.ts @@ -14,6 +14,7 @@ import Settings = require('./settings'); import { Logger } from './logging'; import { IFeature } from './feature'; import { Message } from 'vscode-jsonrpc'; +import { PowerShellProcess } from './process'; import { StringDecoder } from 'string_decoder'; import { LanguageClient, LanguageClientOptions, Executable, @@ -35,16 +36,17 @@ export class SessionManager implements Middleware { private hostVersion: string; private isWindowsOS: boolean; - private sessionFilePath: string; + private editorServicesArgs: string; private powerShellExePath: string = ""; private sessionStatus: SessionStatus; private suppressRestartPrompt: boolean; private focusConsoleOnExecute: boolean; private extensionFeatures: IFeature[] = []; private statusBarItem: vscode.StatusBarItem; + private languageServerProcess: PowerShellProcess; + private debugSessionProcess: PowerShellProcess; private versionDetails: PowerShellVersionDetails; private registeredCommands: vscode.Disposable[] = []; - private consoleTerminal: vscode.Terminal = undefined; private languageServerClient: LanguageClient = undefined; private sessionSettings: Settings.ISettings = undefined; private sessionDetails: utils.EditorServicesSessionDetails; @@ -115,11 +117,6 @@ export class SessionManager implements Middleware { } this.suppressRestartPrompt = false; - - this.sessionFilePath = - utils.getSessionFilePath( - Math.floor(100000 + Math.random() * 900000)); - this.powerShellExePath = this.getPowerShellExePath(); if (this.powerShellExePath) { @@ -144,7 +141,7 @@ export class SessionManager implements Middleware { } } - var startArgs = + this.editorServicesArgs = "-EditorServicesVersion '" + this.requiredEditorServicesVersion + "' " + "-HostName 'Visual Studio Code Host' " + "-HostProfileId 'Microsoft.VSCode' " + @@ -154,17 +151,17 @@ export class SessionManager implements Middleware { "-EnableConsoleRepl "; if (this.sessionSettings.developer.editorServicesWaitForDebugger) { - startArgs += '-WaitForDebugger '; + this.editorServicesArgs += '-WaitForDebugger '; } if (this.sessionSettings.developer.editorServicesLogLevel) { - startArgs += "-LogLevel '" + this.sessionSettings.developer.editorServicesLogLevel + "' " + this.editorServicesArgs += "-LogLevel '" + this.sessionSettings.developer.editorServicesLogLevel + "' " } this.startPowerShell( this.powerShellExePath, this.sessionSettings.developer.powerShellExeIsWindowsDevBuild, bundledModulesPath, - startArgs); + this.editorServicesArgs); } else { this.setSessionFailure("PowerShell could not be started, click 'Show Logs' for more details."); @@ -180,7 +177,7 @@ export class SessionManager implements Middleware { // Before moving further, clear out the client and process if // the process is already dead (i.e. it crashed) this.languageServerClient = undefined; - this.consoleTerminal = undefined; + this.languageServerProcess = undefined; } this.sessionStatus = SessionStatus.Stopping; @@ -191,14 +188,12 @@ export class SessionManager implements Middleware { this.languageServerClient = undefined; } - // Clean up the session file - utils.deleteSessionFile(this.sessionFilePath); - - // Kill the PowerShell process we spawned via the console - if (this.consoleTerminal !== undefined) { - this.log.write(os.EOL + "Terminating PowerShell process..."); - this.consoleTerminal.dispose(); - this.consoleTerminal = undefined; + // Kill the PowerShell proceses we spawned + if (this.debugSessionProcess) { + this.debugSessionProcess.dispose(); + } + if (this.languageServerProcess) { + this.languageServerProcess.dispose(); } this.sessionStatus = SessionStatus.NotStarted; @@ -216,6 +211,22 @@ export class SessionManager implements Middleware { this.registeredCommands.forEach(command => { command.dispose(); }); } + public createDebugSessionProcess( + sessionPath: string, + sessionSettings: Settings.ISettings): PowerShellProcess { + + this.debugSessionProcess = + new PowerShellProcess( + this.powerShellExePath, + "[DBG] PowerShell Integrated Console", + this.log, + this.editorServicesArgs + "-DebugServiceOnly ", + sessionPath, + sessionSettings); + + return this.debugSessionProcess; + } + private onConfigurationUpdated() { var settings = Settings.load(); @@ -271,152 +282,66 @@ export class SessionManager implements Middleware { isWindowsDevBuild: boolean, 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"); - - var featureFlags = - this.sessionSettings.developer.featureFlags !== undefined - ? this.sessionSettings.developer.featureFlags.map(f => `'${f}'`).join(', ') - : ""; - - startArgs += - `-LogPath '${editorServicesLogPath}' ` + - `-SessionDetailsPath '${this.sessionFilePath}' ` + - `-FeatureFlags @(${featureFlags})` - - var powerShellArgs = [ - "-NoProfile", - "-NonInteractive" - ] - - // Only add ExecutionPolicy param on Windows - if (this.isWindowsOS) { - powerShellArgs.push("-ExecutionPolicy", "Unrestricted") - } - - powerShellArgs.push( - "-Command", - "& '" + startScriptPath + "' " + startArgs); - - if (isWindowsDevBuild) { - // Windows PowerShell development builds need the DEVPATH environment - // variable set to the folder where development binaries are held - - // 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'); - fs.writeFileSync( - batScriptPath, - `@set DEVPATH=${path.dirname(powerShellExePath)}\r\n@${powerShellExePath} %*`); - - powerShellExePath = batScriptPath; - } - - this.log.write(`${utils.getTimestampString()} Language server starting...`); - // Make sure no old session file exists - utils.deleteSessionFile(this.sessionFilePath); + this.setSessionStatus( + "Starting PowerShell...", + SessionStatus.Initializing); - // Launch PowerShell in the integrated terminal - this.consoleTerminal = - vscode.window.createTerminal( - "PowerShell Integrated Console", - powerShellExePath, - powerShellArgs); + var sessionFilePath = + utils.getSessionFilePath( + Math.floor(100000 + Math.random() * 900000)); - if (this.sessionSettings.integratedConsole.showOnStartup) { - this.consoleTerminal.show(true); - } + this.languageServerProcess = + new PowerShellProcess( + this.powerShellExePath, + "PowerShell Integrated Console", + this.log, + startArgs, + sessionFilePath, + this.sessionSettings); + + this.languageServerProcess.onExited( + () => { + if (this.sessionStatus === SessionStatus.Running) { + this.setSessionStatus("Session exited", SessionStatus.Failed); + this.promptForRestart(); + } + }); - // Start the language client - utils.waitForSessionFile( - this.sessionFilePath, - (sessionDetails, error) => { + this.languageServerProcess + .start("EditorServices") + .then( + sessionDetails => { this.sessionDetails = sessionDetails; - if (sessionDetails) { - if (sessionDetails.status === "started") { - this.log.write(`${utils.getTimestampString()} Language server started.`); + if (sessionDetails.status === "started") { + this.log.write(`${utils.getTimestampString()} Language server started.`); - // The session file is no longer needed - utils.deleteSessionFile(this.sessionFilePath); - - // Start the language service client - this.startLanguageClient(sessionDetails); + // Start the language service client + this.startLanguageClient(sessionDetails); + } + else if (sessionDetails.status === "failed") { + if (sessionDetails.reason === "unsupported") { + this.setSessionFailure( + `PowerShell language features are only supported on PowerShell version 3 and above. The current version is ${sessionDetails.powerShellVersion}.`) } - else if (sessionDetails.status === "failed") { - if (sessionDetails.reason === "unsupported") { - this.setSessionFailure( - `PowerShell language features are only supported on PowerShell version 3 and above. The current version is ${sessionDetails.powerShellVersion}.`) - } - else if (sessionDetails.reason === "languageMode") { - this.setSessionFailure( - `PowerShell language features are disabled due to an unsupported LanguageMode: ${sessionDetails.detail}`); - } - else { - this.setSessionFailure(`PowerShell could not be started for an unknown reason '${sessionDetails.reason}'`) - } + else if (sessionDetails.reason === "languageMode") { + this.setSessionFailure( + `PowerShell language features are disabled due to an unsupported LanguageMode: ${sessionDetails.detail}`); } else { - // TODO: Handle other response cases + this.setSessionFailure(`PowerShell could not be started for an unknown reason '${sessionDetails.reason}'`) } } else { - this.log.write(`${utils.getTimestampString()} Language server startup failed.`); - this.setSessionFailure("Could not start language service: ", error); - } - }); - - // 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(); - // } - // }); - - vscode.window.onDidCloseTerminal( - terminal => { - if (terminal === this.consoleTerminal) { - this.log.write(os.EOL + "powershell.exe terminated or terminal UI was closed" + os.EOL); - - if (this.sessionStatus === SessionStatus.Running) { - this.setSessionStatus("Session exited", SessionStatus.Failed); - this.promptForRestart(); - } + // TODO: Handle other response cases } - }); - - this.consoleTerminal.processId.then( - pid => { - console.log("powershell.exe started, pid: " + pid + ", exe: " + powerShellExePath); - this.log.write( - "powershell.exe started --", - " pid: " + pid, - " exe: " + powerShellExePath, - " args: " + startScriptPath + ' ' + startArgs + os.EOL + os.EOL); - }); - } - catch (e) - { - this.setSessionFailure("The language service could not be started: ", e); - } + }, + error => { + this.log.write(`${utils.getTimestampString()} Language server startup failed.`); + this.setSessionFailure("The language service could not be started: ", error); + } + ); } private promptForRestart() { @@ -715,8 +640,8 @@ export class SessionManager implements Middleware { } private showSessionConsole(isExecute?: boolean) { - if (this.consoleTerminal) { - this.consoleTerminal.show( + if (this.languageServerProcess) { + this.languageServerProcess.showConsole( isExecute && !this.focusConsoleOnExecute); } } diff --git a/src/settings.ts b/src/settings.ts index d8893a37cf..7934789098 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -32,6 +32,10 @@ export interface IScriptAnalysisSettings { settingsPath: string; } +export interface IDebuggingSettings { + createTemporaryIntegratedConsole?: boolean; +} + export interface IDeveloperSettings { featureFlags?: string[]; powerShellExePath?: string; @@ -47,6 +51,7 @@ export interface ISettings { useX86Host?: boolean; enableProfileLoading?: boolean; scriptAnalysis?: IScriptAnalysisSettings; + debugging?: IDebuggingSettings; developer?: IDeveloperSettings; codeFormatting?: ICodeFormattingSettings; integratedConsole?: IIntegratedConsoleSettings; @@ -67,6 +72,10 @@ export function load(): ISettings { settingsPath: "" }; + let defaultDebuggingSettings: IDebuggingSettings = { + createTemporaryIntegratedConsole: false, + }; + let defaultDeveloperSettings: IDeveloperSettings = { featureFlags: [], powerShellExePath: undefined, @@ -100,6 +109,7 @@ export function load(): ISettings { useX86Host: configuration.get("useX86Host", false), enableProfileLoading: configuration.get("enableProfileLoading", false), scriptAnalysis: configuration.get("scriptAnalysis", defaultScriptAnalysisSettings), + debugging: configuration.get("debugging", defaultDebuggingSettings), developer: configuration.get("developer", defaultDeveloperSettings), codeFormatting: configuration.get("codeFormatting", defaultCodeFormattingSettings), integratedConsole: configuration.get("integratedConsole", defaultIntegratedConsoleSettings) diff --git a/src/utils.ts b/src/utils.ts index 8100cb154b..1fa9e0f30a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -124,3 +124,7 @@ export function getTimestampString() { var time = new Date(); return `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}]` } + +export function isWindowsOS(): boolean { + return os.platform() == "win32"; +} \ No newline at end of file