diff --git a/src/features/DebugSession.ts b/src/features/DebugSession.ts index af23e26f9e..00635e5ced 100644 --- a/src/features/DebugSession.ts +++ b/src/features/DebugSession.ts @@ -8,9 +8,8 @@ import { NotificationType, RequestType } from "vscode-languageclient"; import { LanguageClient } from "vscode-languageclient/node"; import { getPlatformDetails, OperatingSystem } from "../platform"; import { PowerShellProcess} from "../process"; -import { SessionManager, SessionStatus } from "../session"; +import { IEditorServicesSessionDetails, SessionManager, SessionStatus } from "../session"; import Settings = require("../settings"); -import utils = require("../utils"); import { Logger } from "../logging"; import { LanguageClientConsumer } from "../languageClientConsumer"; @@ -25,8 +24,7 @@ export class DebugSessionFeature extends LanguageClientConsumer private sessionCount: number = 1; private tempDebugProcess: PowerShellProcess; - private tempDebugEventHandler: vscode.Disposable; - private tempSessionDetails: utils.IEditorServicesSessionDetails; + private tempSessionDetails: IEditorServicesSessionDetails; constructor(context: ExtensionContext, private sessionManager: SessionManager, private logger: Logger) { super(); @@ -311,15 +309,9 @@ export class DebugSessionFeature extends LanguageClientConsumer // Create or show the interactive console vscode.commands.executeCommand("PowerShell.ShowSessionConsole", true); - const sessionFilePath = utils.getDebugSessionFilePath(); - if (config.createTemporaryIntegratedConsole) { - // TODO: This should be cleaned up to support multiple temporary consoles. - this.tempDebugProcess = this.sessionManager.createDebugSessionProcess(sessionFilePath, settings); + this.tempDebugProcess = this.sessionManager.createDebugSessionProcess(settings); this.tempSessionDetails = await this.tempDebugProcess.start(`DebugSession-${this.sessionCount++}`); - utils.writeSessionFile(sessionFilePath, this.tempSessionDetails); - } else { - utils.writeSessionFile(sessionFilePath, this.sessionManager.getSessionDetails()); } return config; diff --git a/src/features/PesterTests.ts b/src/features/PesterTests.ts index 32cc795a0b..397ab1aba1 100644 --- a/src/features/PesterTests.ts +++ b/src/features/PesterTests.ts @@ -128,12 +128,7 @@ export class PesterTestsFeature implements vscode.Disposable { private async launch(launchConfig): Promise { // 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()); + await vscode.commands.executeCommand("PowerShell.ShowSessionConsole", true); // TODO: Update to handle multiple root workspaces. // diff --git a/src/features/RunCode.ts b/src/features/RunCode.ts index 288a7b1295..4f2855b517 100644 --- a/src/features/RunCode.ts +++ b/src/features/RunCode.ts @@ -35,21 +35,16 @@ export class RunCodeFeature implements vscode.Disposable { const launchType = runInDebugger ? LaunchType.Debug : LaunchType.Run; const launchConfig = createLaunchConfig(launchType, scriptToRun, args); - this.launch(launchConfig); + await this.launch(launchConfig); } - private launch(launchConfig) { + private async launch(launchConfig: string | vscode.DebugConfiguration) { // 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()); + // TODO: #367: Check if "newSession" mode is configured + await vscode.commands.executeCommand("PowerShell.ShowSessionConsole", true); // TODO: Update to handle multiple root workspaces. - vscode.debug.startDebugging(vscode.workspace.workspaceFolders?.[0], launchConfig); + await vscode.debug.startDebugging(vscode.workspace.workspaceFolders?.[0], launchConfig); } } diff --git a/src/logging.ts b/src/logging.ts index b0438a3b48..d98a7e7185 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -5,7 +5,6 @@ import fs = require("fs"); import os = require("os"); import path = require("path"); import vscode = require("vscode"); -import utils = require("./utils"); export enum LogLevel { Diagnostic, @@ -44,7 +43,6 @@ export class Logger implements ILogger { if (logBasePath === undefined) { // No workspace, we have to use another folder. this.logBasePath = vscode.Uri.file(path.resolve(__dirname, "../logs")); - utils.ensurePathExists(this.logBasePath.fsPath); } else { this.logBasePath = vscode.Uri.joinPath(logBasePath, "logs"); } diff --git a/src/process.ts b/src/process.ts index 6c3dba72f5..f98513efb2 100644 --- a/src/process.ts +++ b/src/process.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import fs = require("fs"); import cp = require("child_process"); import * as semver from "semver"; import path = require("path"); @@ -8,10 +9,11 @@ import vscode = require("vscode"); import { Logger } from "./logging"; import Settings = require("./settings"); import utils = require("./utils"); +import { IEditorServicesSessionDetails, SessionManager } from "./session"; export class PowerShellProcess { - public static escapeSingleQuotes(pspath: string): string { - return pspath.replace(new RegExp("'", "g"), "''"); + public static escapeSingleQuotes(psPath: string): string { + return psPath.replace(new RegExp("'", "g"), "''"); } // This is used to warn the user that the extension is taking longer than expected to startup. @@ -30,13 +32,13 @@ export class PowerShellProcess { private title: string, private log: Logger, private startPsesArgs: string, - private sessionFilePath: string, + private sessionFilePath: vscode.Uri, private sessionSettings: Settings.ISettings) { this.onExited = this.onExitedEmitter.event; } - public async start(logFileName: string): Promise { + public async start(logFileName: string): Promise { const editorServicesLogPath = this.log.getLogFilePath(logFileName); const psesModulePath = @@ -52,7 +54,7 @@ export class PowerShellProcess { this.startPsesArgs += `-LogPath '${PowerShellProcess.escapeSingleQuotes(editorServicesLogPath.fsPath)}' ` + - `-SessionDetailsPath '${PowerShellProcess.escapeSingleQuotes(this.sessionFilePath)}' ` + + `-SessionDetailsPath '${PowerShellProcess.escapeSingleQuotes(this.sessionFilePath.fsPath)}' ` + `-FeatureFlags @(${featureFlags}) `; if (this.sessionSettings.integratedConsole.useLegacyReadLine) { @@ -99,7 +101,7 @@ export class PowerShellProcess { " PowerShell Editor Services args: " + startEditorServices); // Make sure no old session file exists - utils.deleteSessionFile(this.sessionFilePath); + await PowerShellProcess.deleteSessionFile(this.sessionFilePath); // Launch PowerShell in the integrated terminal const terminalOptions: vscode.TerminalOptions = { @@ -149,7 +151,7 @@ export class PowerShellProcess { public dispose() { // Clean up the session file - utils.deleteSessionFile(this.sessionFilePath); + PowerShellProcess.deleteSessionFile(this.sessionFilePath); if (this.consoleCloseSubscription) { this.consoleCloseSubscription.dispose(); @@ -189,17 +191,31 @@ export class PowerShellProcess { return true; } - private async waitForSessionFile(): Promise { + private static readSessionFile(sessionFilePath: vscode.Uri): IEditorServicesSessionDetails { + // TODO: Use vscode.workspace.fs.readFile instead of fs.readFileSync. + const fileContents = fs.readFileSync(sessionFilePath.fsPath, "utf-8"); + return JSON.parse(fileContents); + } + + private static async deleteSessionFile(sessionFilePath: vscode.Uri) { + try { + await vscode.workspace.fs.delete(sessionFilePath); + } catch (e) { + // TODO: Be more specific about what we're catching + } + } + + private async waitForSessionFile(): Promise { // Determine how many tries by dividing by 2000 thus checking every 2 seconds. const numOfTries = this.sessionSettings.developer.waitForSessionFileTimeoutSeconds / 2; const warnAt = numOfTries - PowerShellProcess.warnUserThreshold; // Check every 2 seconds for (let i = numOfTries; i > 0; i--) { - if (utils.checkIfFileExists(this.sessionFilePath)) { + if (utils.checkIfFileExists(this.sessionFilePath.fsPath)) { this.log.write("Session file found"); - const sessionDetails = utils.readSessionFile(this.sessionFilePath); - utils.deleteSessionFile(this.sessionFilePath); + const sessionDetails = PowerShellProcess.readSessionFile(this.sessionFilePath); + PowerShellProcess.deleteSessionFile(this.sessionFilePath); return sessionDetails; } diff --git a/src/session.ts b/src/session.ts index 09d93e3cb6..40d576cf56 100644 --- a/src/session.ts +++ b/src/session.ts @@ -16,13 +16,15 @@ import utils = require("./utils"); import { CloseAction, DocumentSelector, ErrorAction, LanguageClientOptions, Middleware, NotificationType, RequestType0, - ResolveCodeLensSignature, RevealOutputChannelOn } from "vscode-languageclient"; + ResolveCodeLensSignature, RevealOutputChannelOn +} from "vscode-languageclient"; import { LanguageClient, StreamInfo } from "vscode-languageclient/node"; import { GitHubReleaseInformation, InvokePowerShellUpdateCheck } from "./features/UpdatePowerShell"; import { getPlatformDetails, IPlatformDetails, IPowerShellExeDetails, - OperatingSystem, PowerShellExeFinder } from "./platform"; + OperatingSystem, PowerShellExeFinder +} from "./platform"; import { LanguageClientConsumer } from "./languageClientConsumer"; export enum SessionStatus { @@ -34,6 +36,20 @@ export enum SessionStatus { Failed, } +export interface IEditorServicesSessionDetails { + status: string; + reason: string; + detail: string; + powerShellVersion: string; + channel: string; + languageServicePort: number; + debugServicePort: number; + languageServicePipeName: string; + debugServicePipeName: string; +} + +export type IReadSessionFileCallback = (details: IEditorServicesSessionDetails) => void; + export class SessionManager implements Middleware { public HostName: string; public HostVersion: string; @@ -55,7 +71,8 @@ export class SessionManager implements Middleware { private registeredCommands: vscode.Disposable[] = []; private languageServerClient: LanguageClient = undefined; private sessionSettings: Settings.ISettings = undefined; - private sessionDetails: utils.IEditorServicesSessionDetails; + private sessionDetails: IEditorServicesSessionDetails; + private sessionsFolder: vscode.Uri; private bundledModulesPath: string; private started: boolean = false; @@ -70,6 +87,14 @@ export class SessionManager implements Middleware { version: string, private telemetryReporter: TelemetryReporter) { + // Create a folder for the session files. + if (extensionContext.storageUri !== undefined) { + this.sessionsFolder = vscode.Uri.joinPath(extensionContext.storageUri, "sessions"); + } else { + this.sessionsFolder = vscode.Uri.file(path.resolve(__dirname, "../sessions")); + } + vscode.workspace.fs.createDirectory(this.sessionsFolder); + this.platformDetails = getPlatformDetails(); this.HostName = hostName; @@ -247,7 +272,7 @@ export class SessionManager implements Middleware { await this.start(exeNameOverride); } - public getSessionDetails(): utils.IEditorServicesSessionDetails { + public getSessionDetails(): IEditorServicesSessionDetails { return this.sessionDetails; } @@ -259,9 +284,12 @@ export class SessionManager implements Middleware { return this.versionDetails; } - public createDebugSessionProcess( - sessionPath: string, - sessionSettings: Settings.ISettings): PowerShellProcess { + public getNewSessionFilePath(): vscode.Uri { + const uniqueId: number = Math.floor(100000 + Math.random() * 900000); + return vscode.Uri.joinPath(this.sessionsFolder, "PSES-VSCode-" + process.env.VSCODE_PID + "-" + uniqueId + ".json"); + } + + public createDebugSessionProcess(sessionSettings: Settings.ISettings): PowerShellProcess { // NOTE: We only support one temporary integrated console at a time. To // support more, we need to track each separately, and tie the session @@ -279,7 +307,7 @@ export class SessionManager implements Middleware { "[TEMP] PowerShell Integrated Console", this.log, this.editorServicesArgs + "-DebugServiceOnly ", - sessionPath, + this.getNewSessionFilePath(), sessionSettings); // Similar to the regular integrated console, we need to send a key @@ -297,7 +325,7 @@ export class SessionManager implements Middleware { } public async waitUntilStarted(): Promise { - while(!this.started) { + while (!this.started) { await utils.sleep(300); } } @@ -308,43 +336,43 @@ export class SessionManager implements Middleware { codeLens: vscode.CodeLens, token: vscode.CancellationToken, next: ResolveCodeLensSignature): vscode.ProviderResult { - const resolvedCodeLens = next(codeLens, token); - const resolveFunc = - (codeLensToFix: vscode.CodeLens): vscode.CodeLens => { - if (codeLensToFix.command?.command === "editor.action.showReferences") { - const oldArgs = codeLensToFix.command.arguments; - - // Our JSON objects don't get handled correctly by - // VS Code's built in editor.action.showReferences - // command so we need to convert them into the - // appropriate types to send them as command - // arguments. - - codeLensToFix.command.arguments = [ - vscode.Uri.parse(oldArgs[0]), - new vscode.Position(oldArgs[1].line, oldArgs[1].character), - oldArgs[2].map((position) => { - return new vscode.Location( - vscode.Uri.parse(position.uri), - new vscode.Range( - position.range.start.line, - position.range.start.character, - position.range.end.line, - position.range.end.character)); - }), - ]; - } + const resolvedCodeLens = next(codeLens, token); + const resolveFunc = + (codeLensToFix: vscode.CodeLens): vscode.CodeLens => { + if (codeLensToFix.command?.command === "editor.action.showReferences") { + const oldArgs = codeLensToFix.command.arguments; + + // Our JSON objects don't get handled correctly by + // VS Code's built in editor.action.showReferences + // command so we need to convert them into the + // appropriate types to send them as command + // arguments. + + codeLensToFix.command.arguments = [ + vscode.Uri.parse(oldArgs[0]), + new vscode.Position(oldArgs[1].line, oldArgs[1].character), + oldArgs[2].map((position) => { + return new vscode.Location( + vscode.Uri.parse(position.uri), + new vscode.Range( + position.range.start.line, + position.range.start.character, + position.range.end.line, + position.range.end.character)); + }), + ]; + } - return codeLensToFix; - }; + return codeLensToFix; + }; - if ((resolvedCodeLens as Thenable).then) { - return (resolvedCodeLens as Thenable).then(resolveFunc); - } else if (resolvedCodeLens as vscode.CodeLens) { - return resolveFunc(resolvedCodeLens as vscode.CodeLens); - } + if ((resolvedCodeLens as Thenable).then) { + return (resolvedCodeLens as Thenable).then(resolveFunc); + } else if (resolvedCodeLens as vscode.CodeLens) { + return resolveFunc(resolvedCodeLens as vscode.CodeLens); + } - return resolvedCodeLens; + return resolvedCodeLens; } // Move old setting codeFormatting.whitespaceAroundPipe to new setting codeFormatting.addWhitespaceAroundPipe @@ -396,13 +424,13 @@ export class SessionManager implements Middleware { // Detect any setting changes that would affect the session if (!this.suppressRestartPrompt && - (settings.cwd.toLowerCase() !== - this.sessionSettings.cwd.toLowerCase() || + (settings.cwd?.toLowerCase() !== + this.sessionSettings.cwd?.toLowerCase() || settings.powerShellDefaultVersion.toLowerCase() !== this.sessionSettings.powerShellDefaultVersion.toLowerCase() || - settings.developer.editorServicesLogLevel.toLowerCase() !== + settings.developer.editorServicesLogLevel.toLowerCase() !== this.sessionSettings.developer.editorServicesLogLevel.toLowerCase() || - settings.developer.bundledModulesPath.toLowerCase() !== + settings.developer.bundledModulesPath.toLowerCase() !== this.sessionSettings.developer.bundledModulesPath.toLowerCase() || settings.integratedConsole.useLegacyReadLine !== this.sessionSettings.integratedConsole.useLegacyReadLine)) { @@ -411,9 +439,9 @@ export class SessionManager implements Middleware { "The PowerShell runtime configuration has changed, would you like to start a new session?", "Yes", "No"); - if (response === "Yes") { - await this.restartSession(); - } + if (response === "Yes") { + await this.restartSession(); + } } } @@ -445,10 +473,6 @@ export class SessionManager implements Middleware { private startPowerShell() { this.setSessionStatus("Starting...", SessionStatus.Initializing); - const sessionFilePath = - utils.getSessionFilePath( - Math.floor(100000 + Math.random() * 900000)); - this.languageServerProcess = new PowerShellProcess( this.PowerShellExeDetails.exePath, @@ -456,7 +480,7 @@ export class SessionManager implements Middleware { "PowerShell Integrated Console", this.log, this.editorServicesArgs, - sessionFilePath, + this.getNewSessionFilePath(), this.sessionSettings); this.languageServerProcess.onExited( @@ -516,7 +540,7 @@ export class SessionManager implements Middleware { } } - private startLanguageClient(sessionDetails: utils.IEditorServicesSessionDetails) { + private startLanguageClient(sessionDetails: IEditorServicesSessionDetails) { // Log the session details object this.log.write(JSON.stringify(sessionDetails)); @@ -531,7 +555,7 @@ export class SessionManager implements Middleware { "connect", () => { this.log.write("Language service connected."); - resolve({writer: socket, reader: socket}); + resolve({ writer: socket, reader: socket }); }); }); }; @@ -540,7 +564,7 @@ export class SessionManager implements Middleware { documentSelector: this.documentSelector, synchronize: { // backend uses "files" and "search" to ignore references. - configurationSection: [ utils.PowerShellLanguageId, "files", "search" ], + configurationSection: [utils.PowerShellLanguageId, "files", "search"], // fileEvents: vscode.workspace.createFileSystemWatcher('**/.eslintrc') }, // NOTE: Some settings are only applicable on startup, so we send them during initialization. @@ -783,8 +807,8 @@ export class SessionManager implements Middleware { case SessionStatus.NeverStarted: case SessionStatus.Stopping: const currentPowerShellExe = - availablePowerShellExes - .find((item) => item.displayName.toLowerCase() === this.PowerShellExeDetails.displayName.toLowerCase()); + availablePowerShellExes + .find((item) => item.displayName.toLowerCase() === this.PowerShellExeDetails.displayName.toLowerCase()); const powerShellSessionName = currentPowerShellExe ? @@ -851,7 +875,7 @@ class SessionMenuItem implements vscode.QuickPickItem { constructor( public readonly label: string, // tslint:disable-next-line:no-empty - public readonly callback: () => void = () => {}) { + public readonly callback: () => void = () => { }) { } } diff --git a/src/utils.ts b/src/utils.ts index e2f4567217..5e60ff2f12 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -10,19 +10,6 @@ import vscode = require("vscode"); export const PowerShellLanguageId = "powershell"; -export function ensurePathExists(targetPath: string): void { - // Ensure that the path exists - try { - // TODO: Use vscode.workspace.fs - fs.mkdirSync(targetPath); - } catch (e) { - // If the exception isn't to indicate that the folder exists already, rethrow it. - if (e.code !== "EEXIST") { - throw e; - } - } -} - // Check that the file exists in an asynchronous manner that relies solely on the VS Code API, not Node's fs library. export async function fileExists(targetPath: string | vscode.Uri): Promise { try { @@ -50,56 +37,6 @@ export function getPipePath(pipeName: string) { } } -export interface IEditorServicesSessionDetails { - status: string; - reason: string; - detail: string; - powerShellVersion: string; - channel: string; - languageServicePort: number; - debugServicePort: number; - languageServicePipeName: string; - debugServicePipeName: string; -} - -export type IReadSessionFileCallback = (details: IEditorServicesSessionDetails) => void; - -const sessionsFolder = path.resolve(__dirname, "../sessions"); -const sessionFilePathPrefix = path.resolve(sessionsFolder, "PSES-VSCode-" + process.env.VSCODE_PID); - -// Create the sessions path if it doesn't exist already -ensurePathExists(sessionsFolder); - -export function getSessionFilePath(uniqueId: number) { - return `${sessionFilePathPrefix}-${uniqueId}`; -} - -export function getDebugSessionFilePath() { - return `${sessionFilePathPrefix}-Debug`; -} - -export function writeSessionFile(sessionFilePath: string, sessionDetails: IEditorServicesSessionDetails) { - ensurePathExists(sessionsFolder); - - const writeStream = fs.createWriteStream(sessionFilePath); - writeStream.write(JSON.stringify(sessionDetails)); - writeStream.close(); -} - - -export function readSessionFile(sessionFilePath: string): IEditorServicesSessionDetails { - const fileContents = fs.readFileSync(sessionFilePath, "utf-8"); - return JSON.parse(fileContents); -} - -export function deleteSessionFile(sessionFilePath: string) { - 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.constants.R_OK); diff --git a/test/core/paths.test.ts b/test/core/paths.test.ts index 8559db5f79..0213b363d7 100644 --- a/test/core/paths.test.ts +++ b/test/core/paths.test.ts @@ -23,7 +23,7 @@ describe("Path assumptions", function () { }); it("Creates the session folder at the correct path", function () { - assert(fs.existsSync(path.resolve(utils.rootPath, "sessions"))); + assert(fs.existsSync(vscode.Uri.joinPath(storageUri, "sessions").fsPath)); }); it("Creates the log folder at the correct path", function () {