Skip to content

Commit

Permalink
Add PowerShell interactive console
Browse files Browse the repository at this point in the history
This change introduces a new terminal-based interactive console experience
in the PowerShell extension.  PowerShell Editor Services is now launched
through VS Code's interactive terminal UI so that it can serve REPL I/O
directly to the user.

This change also modifies the debugger integration such that the same
integrated terminal session is also used during script debugging.

Resolves #293.
  • Loading branch information
daviwil committed Mar 14, 2017
1 parent 68798e5 commit ecdb0ff
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 72 deletions.
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
16 changes: 0 additions & 16 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 @@ -216,18 +215,8 @@ export class ConsoleFeature implements IFeature {
this.languageClient.sendRequest(EvaluateRequest.type, {
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);
})
];

this.consoleChannel = vscode.window.createOutputChannel("PowerShell Output");
}

public setLanguageClient(languageClient: LanguageClient) {
Expand All @@ -240,14 +229,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();
}
}
10 changes: 10 additions & 0 deletions src/features/DebugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export class DebugSessionFeature implements IFeature {
}

private startDebugSession(config: any) {

// TODO: Wait for session to start

if (!config.request) {
// No launch.json, create the default configuration
config.type = 'PowerShell';
Expand All @@ -38,6 +41,13 @@ export class DebugSessionFeature implements IFeature {
config.cwd = config.cwd || vscode.workspace.rootPath || config.script;
}

// Prevent the Debug Console from opening
config.internalConsoleOptions = "neverOpen";

// Create or show the interactive console
// TODO: 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 Interactive 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
28 changes: 28 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ export function getPipePath(pipeName: string) {
}

export interface EditorServicesSessionDetails {
status: string;
reason: string;
powerShellVersion: string;
channel: string;
languageServicePort: number;
debugServicePort: number;
Expand All @@ -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);

Expand All @@ -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)
Expand Down

0 comments on commit ecdb0ff

Please sign in to comment.