Skip to content

Add PowerShell interactive console #529

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions examples/PromptExamples.ps1
Original file line number Diff line number Diff line change
@@ -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)
# 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))
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@
"category": "PowerShell"
},
{
"command": "PowerShell.ShowSessionOutput",
"title": "Show Session Output",
"command": "PowerShell.ShowSessionConsole",
"title": "Show Session Interactive Console",
"category": "PowerShell"
},
{
Expand Down
25 changes: 23 additions & 2 deletions scripts/Start-EditorServices.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,28 @@ param(
[ValidateSet("Normal", "Verbose", "Error")]
$LogLevel,

[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$SessionDetailsPath,

[switch]
$EnableConsoleRepl,

[string]
$DebugServiceOnly,

[switch]
$WaitForDebugger,

[switch]
$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 = @{
Expand All @@ -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;
}
Expand Down Expand Up @@ -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 `
Expand All @@ -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
Expand All @@ -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
Expand Down
17 changes: 2 additions & 15 deletions src/features/Console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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) {
Expand All @@ -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();
}
}
8 changes: 8 additions & 0 deletions src/features/DebugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}
}
Expand Down
122 changes: 70 additions & 52 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 ';
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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(); })
]
}

Expand All @@ -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",
Expand All @@ -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();
Expand All @@ -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)
{
Expand Down Expand Up @@ -595,6 +607,12 @@ export class SessionManager {
return resolvedPath;
}

private showSessionConsole() {
if (this.consoleTerminal) {
this.consoleTerminal.show();
}
}

private showSessionMenu() {
var menuItems: SessionMenuItem[] = [];

Expand Down
Loading