diff --git a/examples/PromptExamples.ps1 b/examples/PromptExamples.ps1 index 959c5c1a16..d2d801af6c 100644 --- a/examples/PromptExamples.ps1 +++ b/examples/PromptExamples.ps1 @@ -1,10 +1,41 @@ +<# ------ Input Prompts ------ #> + +$fields = @( + New-Object "System.Management.Automation.Host.FieldDescription" "Input" + New-Object "System.Management.Automation.Host.FieldDescription" "Input List" +) +$fields[1].SetParameterType([int[]]) + +$host.UI.Prompt("Caption", "Message", $fields) + +Get-Credential +Get-Credential -Message "Test!" +Get-Credential -UserName "myuser" -Message "Password stealer" + +$host.UI.PromptForCredential("Caption", "Message", $null, $null, [System.Management.Automation.PSCredentialTypes]::Default, [System.Management.Automation.PSCredentialUIOptions]::Default) +$host.UI.PromptForCredential("Caption", "Message", "testuser", $null, [System.Management.Automation.PSCredentialTypes]::Default, [System.Management.Automation.PSCredentialUIOptions]::Default) + +Read-Host -AsSecureString +Read-Host -Prompt "Enter a secure string" -AsSecureString + +$field = New-Object "System.Management.Automation.Host.FieldDescription" "SecureString" +$field.SetParameterType([SecureString]) +$host.UI.Prompt("Caption", "Message", $field) + +$field = New-Object "System.Management.Automation.Host.FieldDescription" "PSCredential" +$field.SetParameterType([PSCredential]) +$host.UI.Prompt("Caption", "Message", $field) + +<# ------ Choice Prompts ------ #> -# Multi-choice prompt $choices = @( New-Object "System.Management.Automation.Host.ChoiceDescription" "&Apple", "Apple" New-Object "System.Management.Automation.Host.ChoiceDescription" "&Banana", "Banana" New-Object "System.Management.Automation.Host.ChoiceDescription" "&Orange", "Orange" ) -$defaults = [int[]]@(0, 2) -$host.UI.PromptForChoice("Choose a fruit", "You may choose more than one", $choices, $defaults) \ No newline at end of file +# Single-choice prompt +$host.UI.PromptForChoice("Choose a fruit", "You may choose one", $choices, 1) + +# Multi-choice prompt +$host.UI.PromptForChoice("Choose a fruit", "You may choose more than one", $choices, [int[]]@(0, 2)) \ No newline at end of file diff --git a/package.json b/package.json index 9ef51a8075..93027f475d 100644 --- a/package.json +++ b/package.json @@ -134,8 +134,8 @@ "category": "PowerShell" }, { - "command": "PowerShell.ShowSessionOutput", - "title": "Show Session Output", + "command": "PowerShell.ShowSessionConsole", + "title": "Show Session Interactive Console", "category": "PowerShell" }, { diff --git a/scripts/Start-EditorServices.ps1 b/scripts/Start-EditorServices.ps1 index 94d72a59d7..ff7198aff2 100644 --- a/scripts/Start-EditorServices.ps1 +++ b/scripts/Start-EditorServices.ps1 @@ -47,6 +47,17 @@ param( [ValidateSet("Normal", "Verbose", "Error")] $LogLevel, + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string] + $SessionDetailsPath, + + [switch] + $EnableConsoleRepl, + + [string] + $DebugServiceOnly, + [switch] $WaitForDebugger, @@ -54,6 +65,10 @@ param( $ConfirmInstall ) +function WriteSessionFile($sessionInfo) { + ConvertTo-Json -InputObject $sessionInfo -Compress | Set-Content -Force -Path "$SessionDetailsPath" -ErrorAction Stop +} + # Are we running in PowerShell 2 or earlier? if ($PSVersionTable.PSVersion.Major -le 2) { $resultDetails = @{ @@ -63,7 +78,9 @@ if ($PSVersionTable.PSVersion.Major -le 2) { }; # Notify the client that the services have started - Write-Output (ConvertTo-Json -InputObject $resultDetails -Compress) + WriteSessionFile $resultDetails + + Write-Host "Unsupported PowerShell version $($PSVersionTable.PSVersion), language features are disabled.`n" exit 0; } @@ -181,6 +198,8 @@ else { $languageServicePort = Get-AvailablePort $debugServicePort = Get-AvailablePort +Write-Host "Starting PowerShell...`n" -ForegroundColor Blue + # Create the Editor Services host $editorServicesHost = Start-EditorServicesHost ` @@ -192,6 +211,8 @@ $editorServicesHost = -LanguageServicePort $languageServicePort ` -DebugServicePort $debugServicePort ` -BundledModulesPath $BundledModulesPath ` + -EnableConsoleRepl:$EnableConsoleRepl.IsPresent ` + -DebugServiceOnly:$DebugServiceOnly.IsPresent ` -WaitForDebugger:$WaitForDebugger.IsPresent # TODO: Verify that the service is started @@ -204,7 +225,7 @@ $resultDetails = @{ }; # Notify the client that the services have started -Write-Output (ConvertTo-Json -InputObject $resultDetails -Compress) +WriteSessionFile $resultDetails try { # Wait for the host to complete execution before exiting diff --git a/src/features/Console.ts b/src/features/Console.ts index 145938c44b..b8472b39c6 100644 --- a/src/features/Console.ts +++ b/src/features/Console.ts @@ -190,7 +190,6 @@ function onInputEntered(responseText: string): ShowInputPromptResponseBody { export class ConsoleFeature implements IFeature { private commands: vscode.Disposable[]; private languageClient: LanguageClient; - private consoleChannel: vscode.OutputChannel; constructor() { this.commands = [ @@ -217,17 +216,10 @@ export class ConsoleFeature implements IFeature { expression: editor.document.getText(selectionRange) }); - // Show the output window if it isn't already visible - this.consoleChannel.show(vscode.ViewColumn.Three); - }), - - vscode.commands.registerCommand('PowerShell.ShowSessionOutput', () => { - // Show the output window if it isn't already visible - this.consoleChannel.show(vscode.ViewColumn.Three); + // Show the integrated console if it isn't already visible + vscode.commands.executeCommand("PowerShell.ShowSessionConsole"); }) ]; - - this.consoleChannel = vscode.window.createOutputChannel("PowerShell Output"); } public setLanguageClient(languageClient: LanguageClient) { @@ -240,14 +232,9 @@ export class ConsoleFeature implements IFeature { this.languageClient.onRequest( ShowInputPromptRequest.type, promptDetails => showInputPrompt(promptDetails, this.languageClient)); - - this.languageClient.onNotification(OutputNotification.type, (output) => { - this.consoleChannel.append(output.output); - }); } public dispose() { this.commands.forEach(command => command.dispose()); - this.consoleChannel.dispose(); } } diff --git a/src/features/DebugSession.ts b/src/features/DebugSession.ts index 41771dc024..25ff001a38 100644 --- a/src/features/DebugSession.ts +++ b/src/features/DebugSession.ts @@ -24,6 +24,7 @@ export class DebugSessionFeature implements IFeature { } private startDebugSession(config: any) { + if (!config.request) { // No launch.json, create the default configuration config.type = 'PowerShell'; @@ -57,6 +58,13 @@ export class DebugSessionFeature implements IFeature { } } + // 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'); + vscode.commands.executeCommand('vscode.startDebug', config); } } diff --git a/src/session.ts b/src/session.ts index 01b26c9408..f747338251 100644 --- a/src/session.ts +++ b/src/session.ts @@ -64,11 +64,11 @@ export class SessionManager { 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 consoleTerminal: vscode.Terminal = undefined; private languageServerClient: LanguageClient = undefined; private sessionSettings: Settings.ISettings = undefined; @@ -136,7 +136,8 @@ export class SessionManager { "-HostName 'Visual Studio Code Host' " + "-HostProfileId 'Microsoft.VSCode' " + "-HostVersion '" + this.hostVersion + "' " + - "-BundledModulesPath '" + bundledModulesPath + "' "; + "-BundledModulesPath '" + bundledModulesPath + "' " + + "-EnableConsoleRepl "; if (this.sessionSettings.developer.editorServicesWaitForDebugger) { startArgs += '-WaitForDebugger '; @@ -169,7 +170,7 @@ export class SessionManager { // 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.consoleTerminal = undefined; } this.sessionStatus = SessionStatus.Stopping; @@ -184,10 +185,10 @@ export class SessionManager { utils.deleteSessionFile(); // Kill the PowerShell process we spawned via the console - if (this.powerShellProcess !== undefined) { + if (this.consoleTerminal !== undefined) { this.log.write(os.EOL + "Terminating PowerShell process..."); - this.powerShellProcess.kill(); - this.powerShellProcess = undefined; + this.consoleTerminal.dispose(); + this.consoleTerminal = undefined; } this.sessionStatus = SessionStatus.NotStarted; @@ -242,7 +243,8 @@ export class SessionManager { this.registeredCommands = [ vscode.commands.registerCommand('PowerShell.RestartSession', () => { this.restartSession(); }), vscode.commands.registerCommand(this.ShowSessionMenuCommandName, () => { this.showSessionMenu(); }), - vscode.workspace.onDidChangeConfiguration(() => this.onConfigurationUpdated()) + vscode.workspace.onDidChangeConfiguration(() => this.onConfigurationUpdated()), + vscode.commands.registerCommand('PowerShell.ShowSessionConsole', () => { this.showSessionConsole(); }) ] } @@ -264,7 +266,9 @@ export class SessionManager { var editorServicesLogPath = this.log.getLogFilePath("EditorServices"); - startArgs += "-LogPath '" + editorServicesLogPath + "' "; + startArgs += + "-LogPath '" + editorServicesLogPath + "' " + + "-SessionDetailsPath '" + utils.getSessionFilePath() + "' "; var powerShellArgs = [ "-NoProfile", @@ -291,57 +295,63 @@ export class SessionManager { delete process.env.DEVPATH; } - // Launch PowerShell as child process - this.powerShellProcess = - cp.spawn( + // Make sure no old session file exists + utils.deleteSessionFile(); + + // Launch PowerShell in the integrated terminal + this.consoleTerminal = + vscode.window.createTerminal( + "PowerShell Integrated Console", powerShellExePath, - powerShellArgs, - { env: process.env }); + 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()); + this.consoleTerminal.show(); - if (response["status"] === "started") { - let sessionDetails: utils.EditorServicesSessionDetails = response; + // Start the language client + utils.waitForSessionFile( + (sessionDetails, error) => { + if (sessionDetails) { + if (sessionDetails.status === "started") { + // Write out the session configuration file + utils.writeSessionFile(sessionDetails); - // Start the language service client - this.startLanguageClient(sessionDetails); - } - else if (response["status"] === "failed") { - if (response["reason"] === "unsupported") { - this.setSessionFailure( - `PowerShell language features are only supported on PowerShell version 3 and above. The current version is ${response["powerShellVersion"]}.`) + // 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 { + this.setSessionFailure(`PowerShell could not be started for an unknown reason '${sessionDetails.reason}'`) + } } else { - this.setSessionFailure(`PowerShell could not be started for an unknown reason '${response["reason"]}'`) + // TODO: Handle other response cases } } else { - // TODO: Handle other response cases + this.setSessionFailure("Could not start language service: ", error); } }); - this.powerShellProcess.stderr.on( - 'data', - (data) => { - this.log.writeError("ERROR: " + data); + // 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(); - } - }); + // 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); + vscode.window.onDidCloseTerminal( + terminal => { + this.log.write(os.EOL + "powershell.exe terminated or terminal UI was closed" + os.EOL); if (this.languageServerClient != undefined) { this.languageServerClient.stop(); @@ -353,13 +363,15 @@ export class SessionManager { } }); - 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); + 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) { @@ -595,6 +607,12 @@ export class SessionManager { return resolvedPath; } + private showSessionConsole() { + if (this.consoleTerminal) { + this.consoleTerminal.show(); + } + } + private showSessionMenu() { var menuItems: SessionMenuItem[] = []; diff --git a/src/utils.ts b/src/utils.ts index 15f489f7e9..e474aa0f0d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -55,6 +55,9 @@ export function getPipePath(pipeName: string) { } export interface EditorServicesSessionDetails { + status: string; + reason: string; + powerShellVersion: string; channel: string; languageServicePort: number; debugServicePort: number; @@ -64,6 +67,10 @@ export interface ReadSessionFileCallback { (details: EditorServicesSessionDetails): void; } +export interface WaitForSessionFileCallback { + (details: EditorServicesSessionDetails, error: string): void; +} + let sessionsFolder = path.resolve(__dirname, "..", "sessions/"); let sessionFilePath = path.resolve(sessionsFolder, "PSES-VSCode-" + process.env.VSCODE_PID); @@ -82,6 +89,27 @@ export function writeSessionFile(sessionDetails: EditorServicesSessionDetails) { writeStream.close(); } +export function waitForSessionFile(callback: WaitForSessionFileCallback) { + + function innerTryFunc(remainingTries: number) { + if (remainingTries == 0) { + callback(undefined, "Timed out waiting for session file to appear."); + } + else if(!checkIfFileExists(sessionFilePath)) { + // Wait a bit and try again + setTimeout(function() { innerTryFunc(remainingTries - 1); }, 500); + } + else { + // Session file was found, load and return it + callback(readSessionFile(), undefined); + } + } + + // Since the delay is 500ms, 50 tries gives 25 seconds of time + // for the session file to appear + innerTryFunc(50); +} + export function readSessionFile(): EditorServicesSessionDetails { let fileContents = fs.readFileSync(sessionFilePath, "utf-8"); return JSON.parse(fileContents) diff --git a/vscode-powershell.build.ps1 b/vscode-powershell.build.ps1 index a6a1671edf..133cd8d1f0 100644 --- a/vscode-powershell.build.ps1 +++ b/vscode-powershell.build.ps1 @@ -79,7 +79,7 @@ task Build -Before Package { # If the PSES codebase is co-located, build it first if ($script:psesBuildScriptPath) { Write-Host "`n### Building PowerShellEditorServices`n" -ForegroundColor Green - Invoke-Build BuildHost $script:psesBuildScriptPath + Invoke-Build Build $script:psesBuildScriptPath } Write-Host "`n### Building vscode-powershell" -ForegroundColor Green