diff --git a/.mocharc.json b/.mocharc.json index 1aee445104..2bd21c2f98 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -6,7 +6,7 @@ ".jsx" ], "require": "source-map-support/register", - "timeout": 30000, + "timeout": 60000, "slow": 2000, "spec": "out/test/**/*.test.js" } diff --git a/.vsts-ci/azure-pipelines-ci.yml b/.vsts-ci/azure-pipelines-ci.yml index a06cf0ba60..e1b0866245 100644 --- a/.vsts-ci/azure-pipelines-ci.yml +++ b/.vsts-ci/azure-pipelines-ci.yml @@ -1,4 +1,11 @@ -name: PR-$(System.PullRequest.PullRequestNumber)-$(Date:yyyyMMdd)$(Rev:.rr) +name: CI-$(Build.SourceBranchName)-$(Date:yyyyMMdd)$(Rev:.rr) + +trigger: + - main +pr: + paths: + exclude: + - '**/*.md' variables: # Don't download unneeded packages diff --git a/.vsts-ci/misc-analysis.yml b/.vsts-ci/misc-analysis.yml index bfc33531b2..71b229686f 100644 --- a/.vsts-ci/misc-analysis.yml +++ b/.vsts-ci/misc-analysis.yml @@ -1,12 +1,10 @@ -name: PR-$(System.PullRequest.PullRequestNumber)-$(Date:yyyyMMdd)$(Rev:.rr) - -trigger: - branches: - include: - - main +name: Misc-$(Build.SourceBranchName)-$(Date:yyyyMMdd)$(Rev:.rr) +trigger: none pr: -- main + paths: + exclude: + - '**/*.md' resources: repositories: @@ -23,6 +21,7 @@ jobs: - checkout: self - checkout: ComplianceRepo - template: ci-compliance.yml@ComplianceRepo + # NOTE: This enables our project to work with Visual Studio's Rich Navigation: # https://visualstudio.microsoft.com/services/rich-code-navigation/ - job: RichCodeNav diff --git a/src/features/Console.ts b/src/features/Console.ts index 165716406e..22fcf83913 100644 --- a/src/features/Console.ts +++ b/src/features/Console.ts @@ -176,7 +176,7 @@ export class ConsoleFeature extends LanguageClientConsumer { vscode.commands.registerCommand("PowerShell.RunSelection", async () => { if (vscode.window.activeTerminal && vscode.window.activeTerminal.name !== "PowerShell Extension") { - this.logger.write("PowerShell Extension Terminal is not active! Running in current terminal using 'runSelectedText'"); + this.logger.write("PowerShell Extension Terminal is not active! Running in current terminal using 'runSelectedText'."); await vscode.commands.executeCommand("workbench.action.terminal.runSelectedText"); // We need to honor the focusConsoleOnExecute setting here too. However, the boolean that `show` diff --git a/src/features/DebugSession.ts b/src/features/DebugSession.ts index 23e7e70321..fd684a7758 100644 --- a/src/features/DebugSession.ts +++ b/src/features/DebugSession.ts @@ -4,6 +4,7 @@ import { debug, CancellationToken, + CancellationTokenSource, DebugAdapterDescriptor, DebugAdapterDescriptorFactory, DebugAdapterExecutable, @@ -18,7 +19,6 @@ import { extensions, workspace, commands, - CancellationTokenSource, InputBoxOptions, QuickPickItem, QuickPickOptions, @@ -297,7 +297,7 @@ export class DebugSessionFeature extends LanguageClientConsumer this.sessionManager.showDebugTerminal(true); this.logger.writeVerbose(`Connecting to pipe: ${sessionDetails.debugServicePipeName}`); - this.logger.writeVerbose(`Debug configuration: ${JSON.stringify(session.configuration)}`); + this.logger.writeVerbose(`Debug configuration: ${JSON.stringify(session.configuration, undefined, 2)}`); return new DebugAdapterNamedPipeServer(sessionDetails.debugServicePipeName); } @@ -359,7 +359,10 @@ export class DebugSessionFeature extends LanguageClientConsumer private async createTemporaryIntegratedConsole(session: DebugSession): Promise { const settings = getSettings(); this.tempDebugProcess = await this.sessionManager.createDebugSessionProcess(settings); - this.tempSessionDetails = await this.tempDebugProcess.start(`DebugSession-${this.sessionCount++}`); + // TODO: Maybe set a timeout on the cancellation token? + const cancellationTokenSource = new CancellationTokenSource(); + this.tempSessionDetails = await this.tempDebugProcess.start( + `DebugSession-${this.sessionCount++}`, cancellationTokenSource.token); // NOTE: Dotnet attach debugging is only currently supported if a temporary debug terminal is used, otherwise we get lots of lock conflicts from loading the assemblies. if (session.configuration.attachDotnetDebugger) { @@ -379,16 +382,16 @@ export class DebugSessionFeature extends LanguageClientConsumer // HACK: This seems like you would be calling a method on a variable not assigned yet, but it does work in the flow. // The dispose shorthand demonry for making an event one-time courtesy of: https://github.com/OmniSharp/omnisharp-vscode/blob/b8b07bb12557b4400198895f82a94895cb90c461/test/integrationTests/launchConfiguration.integration.test.ts#L41-L45 startDebugEvent.dispose(); - this.logger.write(`Debugger session detected: ${dotnetAttachSession.name} (${dotnetAttachSession.id})`); + this.logger.writeVerbose(`Debugger session detected: ${dotnetAttachSession.name} (${dotnetAttachSession.id})`); if (dotnetAttachSession.configuration.name == dotnetAttachConfig.name) { const stopDebugEvent = debug.onDidTerminateDebugSession(async (terminatedDebugSession) => { // Makes the event one-time stopDebugEvent.dispose(); - this.logger.write(`Debugger session stopped: ${terminatedDebugSession.name} (${terminatedDebugSession.id})`); + this.logger.writeVerbose(`Debugger session stopped: ${terminatedDebugSession.name} (${terminatedDebugSession.id})`); if (terminatedDebugSession === session) { - this.logger.write("Terminating dotnet debugger session associated with PowerShell debug session"); + this.logger.writeVerbose("Terminating dotnet debugger session associated with PowerShell debug session!"); await debug.stopDebugging(dotnetAttachSession); } }); @@ -398,8 +401,8 @@ export class DebugSessionFeature extends LanguageClientConsumer // Start a child debug session to attach the dotnet debugger // TODO: Accommodate multi-folder workspaces if the C# code is in a different workspace folder await debug.startDebugging(undefined, dotnetAttachConfig, session); - this.logger.writeVerbose(`Dotnet Attach Debug configuration: ${JSON.stringify(dotnetAttachConfig)}`); - this.logger.write(`Attached dotnet debugger to process ${pid}`); + this.logger.writeVerbose(`Dotnet attach debug configuration: ${JSON.stringify(dotnetAttachConfig, undefined, 2)}`); + this.logger.writeVerbose(`Attached dotnet debugger to process: ${pid}`); } return this.tempSessionDetails; } diff --git a/src/features/ExtensionCommands.ts b/src/features/ExtensionCommands.ts index 40bf4272e2..40f706e50f 100644 --- a/src/features/ExtensionCommands.ts +++ b/src/features/ExtensionCommands.ts @@ -439,7 +439,7 @@ export class ExtensionCommandsFeature extends LanguageClientConsumer { default: { // Other URI schemes are not supported - const msg = JSON.stringify(saveFileDetails); + const msg = JSON.stringify(saveFileDetails, undefined, 2); this.logger.writeVerbose( `<${ExtensionCommandsFeature.name}>: Saving a document with scheme '${currentFileUri.scheme}' ` + `is currently unsupported. Message: '${msg}'`); @@ -467,9 +467,9 @@ export class ExtensionCommandsFeature extends LanguageClientConsumer { await vscode.workspace.fs.writeFile( vscode.Uri.file(destinationAbsolutePath), Buffer.from(oldDocument.getText())); - } catch (e) { + } catch (err) { void this.logger.writeAndShowWarning(`<${ExtensionCommandsFeature.name}>: ` + - `Unable to save file to path '${destinationAbsolutePath}': ${e}`); + `Unable to save file to path '${destinationAbsolutePath}': ${err}`); return; } diff --git a/src/features/ExternalApi.ts b/src/features/ExternalApi.ts index 3643ad2821..913d8e25f3 100644 --- a/src/features/ExternalApi.ts +++ b/src/features/ExternalApi.ts @@ -57,7 +57,7 @@ export class ExternalApiFeature extends LanguageClientConsumer implements IPower string session uuid */ public registerExternalExtension(id: string, apiVersion = "v1"): string { - this.logger.writeDiagnostic(`Registering extension '${id}' for use with API version '${apiVersion}'.`); + this.logger.writeVerbose(`Registering extension '${id}' for use with API version '${apiVersion}'.`); // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const [_name, externalExtension] of ExternalApiFeature.registeredExternalExtension) { @@ -98,7 +98,7 @@ export class ExternalApiFeature extends LanguageClientConsumer implements IPower true if it worked, otherwise throws an error. */ public unregisterExternalExtension(uuid = ""): boolean { - this.logger.writeDiagnostic(`Unregistering extension with session UUID: ${uuid}`); + this.logger.writeVerbose(`Unregistering extension with session UUID: ${uuid}`); if (!ExternalApiFeature.registeredExternalExtension.delete(uuid)) { throw new Error(`No extension registered with session UUID: ${uuid}`); } @@ -135,7 +135,7 @@ export class ExternalApiFeature extends LanguageClientConsumer implements IPower */ public async getPowerShellVersionDetails(uuid = ""): Promise { const extension = this.getRegisteredExtension(uuid); - this.logger.writeDiagnostic(`Extension '${extension.id}' called 'getPowerShellVersionDetails'`); + this.logger.writeVerbose(`Extension '${extension.id}' called 'getPowerShellVersionDetails'.`); await this.sessionManager.waitUntilStarted(); const versionDetails = this.sessionManager.getPowerShellVersionDetails(); @@ -163,7 +163,7 @@ export class ExternalApiFeature extends LanguageClientConsumer implements IPower */ public async waitUntilStarted(uuid = ""): Promise { const extension = this.getRegisteredExtension(uuid); - this.logger.writeDiagnostic(`Extension '${extension.id}' called 'waitUntilStarted'`); + this.logger.writeVerbose(`Extension '${extension.id}' called 'waitUntilStarted'.`); await this.sessionManager.waitUntilStarted(); } diff --git a/src/features/GetCommands.ts b/src/features/GetCommands.ts index 144240bb1b..2e0031c2af 100644 --- a/src/features/GetCommands.ts +++ b/src/features/GetCommands.ts @@ -65,7 +65,7 @@ export class GetCommandsFeature extends LanguageClientConsumer { private async CommandExplorerRefresh(): Promise { if (this.languageClient === undefined) { - this.logger.writeVerbose(`<${GetCommandsFeature.name}>: Unable to send getCommand request`); + this.logger.writeVerbose(`<${GetCommandsFeature.name}>: Unable to send getCommand request!`); return; } await this.languageClient.sendRequest(GetCommandRequestType).then((result) => { diff --git a/src/features/UpdatePowerShell.ts b/src/features/UpdatePowerShell.ts index 9b3731f3e1..7e9627a87e 100644 --- a/src/features/UpdatePowerShell.ts +++ b/src/features/UpdatePowerShell.ts @@ -51,20 +51,20 @@ export class UpdatePowerShell { private shouldCheckForUpdate(): boolean { // Respect user setting. if (!this.sessionSettings.promptToUpdatePowerShell) { - this.logger.writeDiagnostic("Setting 'promptToUpdatePowerShell' was false."); + this.logger.writeVerbose("Setting 'promptToUpdatePowerShell' was false."); return false; } // Respect environment configuration. if (process.env.POWERSHELL_UPDATECHECK?.toLowerCase() === "off") { - this.logger.writeDiagnostic("Environment variable 'POWERSHELL_UPDATECHECK' was 'Off'."); + this.logger.writeVerbose("Environment variable 'POWERSHELL_UPDATECHECK' was 'Off'."); return false; } // Skip prompting when using Windows PowerShell for now. if (this.localVersion.compare("6.0.0") === -1) { // TODO: Maybe we should announce PowerShell Core? - this.logger.writeDiagnostic("Not offering to update Windows PowerShell."); + this.logger.writeVerbose("Not prompting to update Windows PowerShell."); return false; } @@ -78,27 +78,26 @@ export class UpdatePowerShell { // Skip if PowerShell is self-built, that is, this contains a commit hash. if (commit.length >= 40) { - this.logger.writeDiagnostic("Not offering to update development build."); + this.logger.writeVerbose("Not prompting to update development build."); return false; } // Skip if preview is a daily build. if (daily.toLowerCase().startsWith("daily")) { - this.logger.writeDiagnostic("Not offering to update daily build."); + this.logger.writeVerbose("Not prompting to update daily build."); return false; } } // TODO: Check if network is available? // TODO: Only check once a week. - this.logger.writeDiagnostic("Should check for PowerShell update."); return true; } - private async getRemoteVersion(url: string): Promise { + private async getRemoteVersion(url: string): Promise { const response = await fetch(url); if (!response.ok) { - throw new Error("Failed to get remote version!"); + return undefined; } // Looks like: // { @@ -107,7 +106,7 @@ export class UpdatePowerShell { // "ReleaseTag": "v7.2.7" // } const data = await response.json(); - this.logger.writeDiagnostic(`From '${url}' received:\n${data}`); + this.logger.writeVerbose(`Received from '${url}':\n${JSON.stringify(data, undefined, 2)}`); return data.ReleaseTag; } @@ -116,30 +115,40 @@ export class UpdatePowerShell { return undefined; } + this.logger.writeVerbose("Checking for PowerShell update..."); const tags: string[] = []; if (process.env.POWERSHELL_UPDATECHECK?.toLowerCase() === "lts") { // Only check for update to LTS. - this.logger.writeDiagnostic("Checking for LTS update."); - tags.push(await this.getRemoteVersion(UpdatePowerShell.LTSBuildInfoURL)); + this.logger.writeVerbose("Checking for LTS update..."); + const tag = await this.getRemoteVersion(UpdatePowerShell.LTSBuildInfoURL); + if (tag != undefined) { + tags.push(tag); + } } else { // Check for update to stable. - this.logger.writeDiagnostic("Checking for stable update."); - tags.push(await this.getRemoteVersion(UpdatePowerShell.StableBuildInfoURL)); + this.logger.writeVerbose("Checking for stable update..."); + const tag = await this.getRemoteVersion(UpdatePowerShell.StableBuildInfoURL); + if (tag != undefined) { + tags.push(tag); + } // Also check for a preview update. if (this.localVersion.prerelease.length > 0) { - this.logger.writeDiagnostic("Checking for preview update."); - tags.push(await this.getRemoteVersion(UpdatePowerShell.PreviewBuildInfoURL)); + this.logger.writeVerbose("Checking for preview update..."); + const tag = await this.getRemoteVersion(UpdatePowerShell.PreviewBuildInfoURL); + if (tag != undefined) { + tags.push(tag); + } } } for (const tag of tags) { if (this.localVersion.compare(tag) === -1) { - this.logger.writeDiagnostic(`Offering to update PowerShell to ${tag}.`); return tag; } } + this.logger.write("PowerShell is up-to-date."); return undefined; } @@ -147,7 +156,7 @@ export class UpdatePowerShell { try { const tag = await this.maybeGetNewRelease(); if (tag) { - return await this.installUpdate(tag); + return await this.promptToUpdate(tag); } } catch (err) { // Best effort. This probably failed to fetch the data from GitHub. @@ -160,21 +169,22 @@ export class UpdatePowerShell { await vscode.env.openExternal(url); } - private async installUpdate(tag: string): Promise { + private async promptToUpdate(tag: string): Promise { const releaseVersion = new SemVer(tag); + this.logger.write(`Prompting to update PowerShell v${this.localVersion.version} to v${releaseVersion.version}.`); const result = await vscode.window.showInformationMessage( - `You have an old version of PowerShell (${this.localVersion.version}). - The current latest release is ${releaseVersion.version}. - Would you like to open the GitHub release in your browser?`, + `PowerShell v${this.localVersion.version} is out-of-date. + The latest version is v${releaseVersion.version}. + Would you like to open the GitHub release in your browser?`, ...UpdatePowerShell.promptOptions); // If the user cancels the notification. if (!result) { - this.logger.writeDiagnostic("User canceled PowerShell update prompt."); + this.logger.writeVerbose("User canceled PowerShell update prompt."); return; } - this.logger.writeDiagnostic(`User said '${UpdatePowerShell.promptOptions[result.id].title}'.`); + this.logger.writeVerbose(`User said '${UpdatePowerShell.promptOptions[result.id].title}'.`); switch (result.id) { // Yes diff --git a/src/logging.ts b/src/logging.ts index 87f88dad61..6a8b984f74 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -57,7 +57,7 @@ export class Logger implements ILogger { // Early logging of the log paths for debugging. if (LogLevel.Diagnostic >= this.logLevel) { - const uriMessage = Logger.timestampMessage(`Global storage URI: '${globalStorageUri}', log file path: '${this.logFilePath}'`, LogLevel.Diagnostic); + const uriMessage = Logger.timestampMessage(`Log file path: '${this.logFilePath}'`, LogLevel.Verbose); this.logChannel.appendLine(uriMessage); } @@ -211,7 +211,7 @@ export class Logger implements ILogger { try { this.writingLog = true; if (!this.logDirectoryCreated) { - this.logChannel.appendLine(Logger.timestampMessage(`Creating log directory at: '${this.logDirectoryPath}'`, level)); + this.writeVerbose(`Creating log directory at: '${this.logDirectoryPath}'`); await vscode.workspace.fs.createDirectory(this.logDirectoryPath); this.logDirectoryCreated = true; } @@ -222,8 +222,8 @@ export class Logger implements ILogger { await vscode.workspace.fs.writeFile( this.logFilePath, Buffer.concat([log, Buffer.from(timestampedMessage)])); - } catch (e) { - console.log(`Error writing to vscode-powershell log file: ${e}`); + } catch (err) { + console.log(`Error writing to vscode-powershell log file: ${err}`); } finally { this.writingLog = false; } diff --git a/src/main.ts b/src/main.ts index 1b0be4c067..95365bff74 100644 --- a/src/main.ts +++ b/src/main.ts @@ -59,7 +59,7 @@ export async function activate(context: vscode.ExtensionContext): Promise; private onExitedEmitter = new vscode.EventEmitter(); @@ -21,6 +20,8 @@ export class PowerShellProcess { private consoleTerminal?: vscode.Terminal; private consoleCloseSubscription?: vscode.Disposable; + private pid?: number; + constructor( public exePath: string, private bundledModulesPath: string, @@ -33,7 +34,7 @@ export class PowerShellProcess { this.onExited = this.onExitedEmitter.event; } - public async start(logFileName: string): Promise { + public async start(logFileName: string, cancellationToken: vscode.CancellationToken): Promise { const editorServicesLogPath = this.logger.getLogFilePath(logFileName); const psesModulePath = @@ -86,19 +87,16 @@ export class PowerShellProcess { startEditorServices); } else { // Otherwise use -EncodedCommand for better quote support. + this.logger.writeVerbose("Using Base64 -EncodedCommand but logging as -Command equivalent."); powerShellArgs.push( "-EncodedCommand", Buffer.from(startEditorServices, "utf16le").toString("base64")); } - this.logger.write( - "Language server starting --", - " PowerShell executable: " + this.exePath, - " PowerShell args: " + powerShellArgs.join(" "), - " PowerShell Editor Services args: " + startEditorServices); + this.logger.writeVerbose(`Starting process: ${this.exePath} ${powerShellArgs.slice(0, -2).join(" ")} -Command ${startEditorServices}`); // Make sure no old session file exists - await PowerShellProcess.deleteSessionFile(this.sessionFilePath); + await this.deleteSessionFile(this.sessionFilePath); // Launch PowerShell in the integrated terminal const terminalOptions: vscode.TerminalOptions = { @@ -116,15 +114,9 @@ export class PowerShellProcess { // subscription should happen before we create the terminal so if it // fails immediately, the event fires. this.consoleCloseSubscription = vscode.window.onDidCloseTerminal((terminal) => this.onTerminalClose(terminal)); - this.consoleTerminal = vscode.window.createTerminal(terminalOptions); - - const pwshName = path.basename(this.exePath); - this.logger.write(`${pwshName} started.`); - - // Log that the PowerShell terminal process has been started - const pid = await this.getPid(); - this.logTerminalPid(pid ?? 0, pwshName); + this.pid = await this.getPid(); + this.logger.write(`PowerShell process started with PID: ${this.pid}`); if (this.sessionSettings.integratedConsole.showOnStartup && !this.sessionSettings.integratedConsole.startInBackground) { @@ -132,7 +124,7 @@ export class PowerShellProcess { this.consoleTerminal.show(true); } - return await this.waitForSessionFile(); + return await this.waitForSessionFile(cancellationToken); } // This function should only be used after a failure has occurred because it is slow! @@ -144,25 +136,23 @@ export class PowerShellProcess { // Returns the process Id of the consoleTerminal public async getPid(): Promise { - if (!this.consoleTerminal) { return undefined; } - return await this.consoleTerminal.processId; + return await this.consoleTerminal?.processId; } public showTerminal(preserveFocus?: boolean): void { this.consoleTerminal?.show(preserveFocus); } - public async dispose(): Promise { - // Clean up the session file - this.logger.write("Disposing PowerShell Extension Terminal..."); + public dispose(): void { + this.logger.writeVerbose(`Disposing PowerShell process with PID: ${this.pid}`); + + void this.deleteSessionFile(this.sessionFilePath); this.consoleTerminal?.dispose(); this.consoleTerminal = undefined; this.consoleCloseSubscription?.dispose(); this.consoleCloseSubscription = undefined; - - await PowerShellProcess.deleteSessionFile(this.sessionFilePath); } public sendKeyPress(): void { @@ -173,10 +163,6 @@ export class PowerShellProcess { this.consoleTerminal?.sendText("p", false); } - private logTerminalPid(pid: number, exeName: string): void { - this.logger.write(`${exeName} PID: ${pid}`); - } - private isLoginShell(pwshPath: string): boolean { try { // We can't know what version of PowerShell we have without running it @@ -187,16 +173,15 @@ export class PowerShellProcess { } catch { return false; } - return true; } - private static async readSessionFile(sessionFilePath: vscode.Uri): Promise { + private async readSessionFile(sessionFilePath: vscode.Uri): Promise { const fileContents = await vscode.workspace.fs.readFile(sessionFilePath); return JSON.parse(fileContents.toString()); } - private static async deleteSessionFile(sessionFilePath: vscode.Uri): Promise { + private async deleteSessionFile(sessionFilePath: vscode.Uri): Promise { try { await vscode.workspace.fs.delete(sessionFilePath); } catch { @@ -204,38 +189,38 @@ export class PowerShellProcess { } } - private async waitForSessionFile(): Promise { - // Determine how many tries by dividing by 2000 thus checking every 2 seconds. - const numOfTries = this.sessionSettings.developer.waitForSessionFileTimeoutSeconds / 2; + private async waitForSessionFile(cancellationToken: vscode.CancellationToken): Promise { + const numOfTries = this.sessionSettings.developer.waitForSessionFileTimeoutSeconds; const warnAt = numOfTries - PowerShellProcess.warnUserThreshold; - // Check every 2 seconds - this.logger.write("Waiting for session file..."); + // Check every second. + this.logger.writeVerbose(`Waiting for session file: ${this.sessionFilePath}`); for (let i = numOfTries; i > 0; i--) { + if (cancellationToken.isCancellationRequested) { + this.logger.writeWarning("Canceled while waiting for session file."); + return undefined; + } + if (this.consoleTerminal === undefined) { - const err = "PowerShell Extension Terminal didn't start!"; - this.logger.write(err); - throw new Error(err); + this.logger.writeError("Extension Terminal is undefined."); + return undefined; } if (await utils.checkIfFileExists(this.sessionFilePath)) { - this.logger.write("Session file found!"); - const sessionDetails = await PowerShellProcess.readSessionFile(this.sessionFilePath); - await PowerShellProcess.deleteSessionFile(this.sessionFilePath); - return sessionDetails; + this.logger.writeVerbose("Session file found."); + return await this.readSessionFile(this.sessionFilePath); } if (warnAt === i) { void this.logger.writeAndShowWarning("Loading the PowerShell extension is taking longer than expected. If you're using privilege enforcement software, this can affect start up performance."); } - // Wait a bit and try again - await utils.sleep(2000); + // Wait a bit and try again. + await utils.sleep(1000); } - const err = "Timed out waiting for session file to appear!"; - this.logger.write(err); - throw new Error(err); + this.logger.writeError("Timed out waiting for session file!"); + return undefined; } private onTerminalClose(terminal: vscode.Terminal): void { @@ -243,8 +228,8 @@ export class PowerShellProcess { return; } - this.logger.write("PowerShell process terminated or Extension Terminal was closed!"); + this.logger.writeWarning(`PowerShell process terminated or Extension Terminal was closed, PID: ${this.pid}`); this.onExitedEmitter.fire(); - void this.dispose(); + this.dispose(); } } diff --git a/src/session.ts b/src/session.ts index 1925cf8a0f..d7ff4adce7 100644 --- a/src/session.ts +++ b/src/session.ts @@ -27,13 +27,12 @@ import { LanguageClientConsumer } from "./languageClientConsumer"; import { SemVer, satisfies } from "semver"; export enum SessionStatus { - NeverStarted, - NotStarted, - Initializing, - Running, - Busy, - Stopping, - Failed, + NotStarted = "Not Started", + Starting = "Starting", + Running = "Running", + Busy = "Busy", + Stopping = "Stopping", + Failed = "Failed", } export enum RunspaceType { @@ -77,23 +76,22 @@ export class SessionManager implements Middleware { public HostVersion: string; public Publisher: string; public PowerShellExeDetails: IPowerShellExeDetails | undefined; - private ShowSessionMenuCommandName = "PowerShell.ShowSessionMenu"; - private sessionStatus: SessionStatus = SessionStatus.NeverStarted; - private suppressRestartPrompt = false; - private platformDetails: IPlatformDetails; + private readonly ShowSessionMenuCommandName = "PowerShell.ShowSessionMenu"; + private debugEventHandler: vscode.Disposable | undefined; + private debugSessionProcess: PowerShellProcess | undefined; + private languageClient: LanguageClient | undefined; private languageClientConsumers: LanguageClientConsumer[] = []; - private languageStatusItem: vscode.LanguageStatusItem; private languageServerProcess: PowerShellProcess | undefined; - private debugSessionProcess: PowerShellProcess | undefined; - private debugEventHandler: vscode.Disposable | undefined; - private versionDetails: IPowerShellVersionDetails | undefined; - private registeredHandlers: vscode.Disposable[] = []; + private languageStatusItem: vscode.LanguageStatusItem; + private platformDetails: IPlatformDetails; private registeredCommands: vscode.Disposable[] = []; - private languageClient: LanguageClient | undefined; + private registeredHandlers: vscode.Disposable[] = []; private sessionDetails: IEditorServicesSessionDetails | undefined; private sessionsFolder: vscode.Uri; - private starting = false; - private started = false; + private sessionStatus: SessionStatus = SessionStatus.NotStarted; + private startCancellationTokenSource: vscode.CancellationTokenSource | undefined; + private suppressRestartPrompt = false; + private versionDetails: IPowerShellVersionDetails | undefined; constructor( private extensionContext: vscode.ExtensionContext, @@ -121,9 +119,9 @@ export class SessionManager implements Middleware { const procBitness = this.platformDetails.isProcess64Bit ? "64-bit" : "32-bit"; this.logger.write( - `Visual Studio Code v${vscode.version} ${procBitness}`, - `${this.DisplayName} Extension v${this.HostVersion}`, - `Operating System: ${OperatingSystem[this.platformDetails.operatingSystem]} ${osBitness}`); + `Visual Studio Code: v${vscode.version} ${procBitness}` + + ` on ${OperatingSystem[this.platformDetails.operatingSystem]} ${osBitness}`, + `${this.DisplayName} Extension: v${this.HostVersion}`); // Fix the host version so that PowerShell can consume it. // This is needed when the extension uses a prerelease @@ -134,7 +132,9 @@ export class SessionManager implements Middleware { } public async dispose(): Promise { - await this.stop(); + await this.stop(); // A whole lot of disposals. + + this.languageStatusItem.dispose(); for (const handler of this.registeredHandlers) { handler.dispose(); @@ -143,72 +143,120 @@ export class SessionManager implements Middleware { for (const command of this.registeredCommands) { command.dispose(); } - - await this.languageClient?.dispose(); - } - - public setLanguageClientConsumers(languageClientConsumers: LanguageClientConsumer[]): void { - this.languageClientConsumers = languageClientConsumers; } // The `exeNameOverride` is used by `restartSession` to override ANY other setting. public async start(exeNameOverride?: string): Promise { // A simple lock because this function isn't re-entrant. - if (this.started || this.starting) { + if (this.sessionStatus === SessionStatus.Starting) { + this.logger.writeWarning("Re-entered 'start' so waiting..."); return await this.waitUntilStarted(); } - try { - this.starting = true; - if (exeNameOverride) { - this.sessionSettings.powerShellDefaultVersion = exeNameOverride; + + this.setSessionStatus("Starting...", SessionStatus.Starting); + this.startCancellationTokenSource = new vscode.CancellationTokenSource(); + const cancellationToken = this.startCancellationTokenSource.token; + + if (exeNameOverride != undefined) { + this.logger.writeVerbose(`Starting with executable overriden to: ${exeNameOverride}`); + this.sessionSettings.powerShellDefaultVersion = exeNameOverride; + } + + // Create a folder for the session files. + await vscode.workspace.fs.createDirectory(this.sessionsFolder); + + // Migrate things. + await this.promptPowerShellExeSettingsCleanup(); + await this.migrateWhitespaceAroundPipeSetting(); + + // Find the PowerShell executable to use for the server. + this.PowerShellExeDetails = await this.findPowerShell(); + + if (this.PowerShellExeDetails === undefined) { + const message = "Unable to find PowerShell!" + + " Do you have it installed?" + + " You can also configure custom installations" + + " with the 'powershell.powerShellAdditionalExePaths' setting."; + void this.setSessionFailedGetPowerShell(message); + return; + } + + this.logger.write(`Starting '${this.PowerShellExeDetails.displayName}' at: ${this.PowerShellExeDetails.exePath}`); + + // Start the server. + this.languageServerProcess = await this.startLanguageServerProcess( + this.PowerShellExeDetails, + cancellationToken); + + // Check that we got session details and that they had a "started" status. + if (this.sessionDetails === undefined || !this.sessionStarted(this.sessionDetails)) { + if (!cancellationToken.isCancellationRequested) { + // If it failed but we didn't cancel it, handle the common reasons. + await this.handleFailedProcess(this.languageServerProcess); } - // Create a folder for the session files. - await vscode.workspace.fs.createDirectory(this.sessionsFolder); - await this.promptPowerShellExeSettingsCleanup(); - await this.migrateWhitespaceAroundPipeSetting(); - this.PowerShellExeDetails = await this.findPowerShell(); - this.languageServerProcess = await this.startPowerShell(); - } finally { - this.starting = false; + this.languageServerProcess.dispose(); + this.languageServerProcess = undefined; + return; + } + + // If we got good session details from the server, try to connect to it. + this.languageClient = await this.startLanguageClient(this.sessionDetails); + + if (this.languageClient.isRunning()) { + this.versionDetails = await this.getVersionDetails(); + if (this.versionDetails === undefined) { + void this.setSessionFailedOpenBug("Unable to get version details!"); + return; + } + + this.logger.write(`Started PowerShell v${this.versionDetails.version}.`); + this.setSessionRunningStatus(); // Yay, we made it! + + // Fire and forget the updater. + const updater = new UpdatePowerShell(this.sessionSettings, this.logger, this.versionDetails); + void updater.checkForUpdate(); + } else { + void this.setSessionFailedOpenBug("Never finished startup!"); } } public async stop(): Promise { - this.logger.write("Shutting down language client..."); + this.setSessionStatus("Stopping...", SessionStatus.Stopping); + // Cancel start-up if we're still waiting. + this.startCancellationTokenSource?.cancel(); + // Stop and dispose the language client. try { - if (this.sessionStatus === SessionStatus.Failed) { - // Before moving further, clear out the client and process if - // the process is already dead (i.e. it crashed). - await this.languageClient?.dispose(); - this.languageClient = undefined; - await this.languageServerProcess?.dispose(); - this.languageServerProcess = undefined; - } + // If the stop fails, so will the dispose, I think this is a bug in + // the client library. + await this.languageClient?.stop(3000); + await this.languageClient?.dispose(); + } catch (err) { + this.logger.writeError(`Error occurred while stopping language client:\n${err}`); + } - this.sessionStatus = SessionStatus.Stopping; + this.languageClient = undefined; - // Stop the language client. - await this.languageClient?.stop(); - await this.languageClient?.dispose(); - this.languageClient = undefined; + // Stop and dispose the PowerShell process(es). + this.debugSessionProcess?.dispose(); + this.debugSessionProcess = undefined; + this.debugEventHandler?.dispose(); + this.debugEventHandler = undefined; - // Kill the PowerShell process(es) we spawned. - await this.debugSessionProcess?.dispose(); - this.debugSessionProcess = undefined; - this.debugEventHandler?.dispose(); - this.debugEventHandler = undefined; + this.languageServerProcess?.dispose(); + this.languageServerProcess = undefined; - await this.languageServerProcess?.dispose(); - this.languageServerProcess = undefined; + // Clean up state to start again. + this.startCancellationTokenSource?.dispose(); + this.startCancellationTokenSource = undefined; + this.sessionDetails = undefined; + this.versionDetails = undefined; - } finally { - this.sessionStatus = SessionStatus.NotStarted; - this.started = false; - } + this.setSessionStatus("Not Started", SessionStatus.NotStarted); } public async restartSession(exeNameOverride?: string): Promise { + this.logger.write("Restarting session..."); await this.stop(); // Re-load and validate the settings. @@ -219,13 +267,11 @@ export class SessionManager implements Middleware { } public getSessionDetails(): IEditorServicesSessionDetails | undefined { - const sessionDetails = this.sessionDetails; - if (sessionDetails != undefined) { - return sessionDetails; - } else { - void this.logger.writeAndShowError("Editor Services session details are not available yet."); - return undefined; + // TODO: This is used solely by the debugger and should actually just wait (with a timeout). + if (this.sessionDetails === undefined) { + void this.logger.writeAndShowError("PowerShell session unavailable for debugging!"); } + return this.sessionDetails; } public getSessionStatus(): SessionStatus { @@ -241,12 +287,16 @@ export class SessionManager implements Middleware { return vscode.Uri.joinPath(this.sessionsFolder, `PSES-VSCode-${process.env.VSCODE_PID}-${uniqueId}.json`); } + public setLanguageClientConsumers(languageClientConsumers: LanguageClientConsumer[]): void { + this.languageClientConsumers = languageClientConsumers; + } + public async createDebugSessionProcess(settings: Settings): Promise { // NOTE: We only support one temporary Extension Terminal at a time. To // support more, we need to track each separately, and tie the session // for the event handler to the right process (and dispose of the event // handler when the process is disposed). - await this.debugSessionProcess?.dispose(); + this.debugSessionProcess?.dispose(); this.debugEventHandler?.dispose(); if (this.PowerShellExeDetails === undefined) { @@ -265,7 +315,7 @@ export class SessionManager implements Middleware { bundledModulesPath, "[TEMP] PowerShell Extension", this.logger, - this.buildEditorServicesArgs(bundledModulesPath, this.PowerShellExeDetails) + "-DebugServiceOnly ", + this.getEditorServicesArgs(bundledModulesPath, this.PowerShellExeDetails) + "-DebugServiceOnly ", this.getNewSessionFilePath(), this.sessionSettings); @@ -284,7 +334,10 @@ export class SessionManager implements Middleware { } public async waitUntilStarted(): Promise { - while (!this.started) { + while (this.sessionStatus === SessionStatus.Starting) { + if (this.startCancellationTokenSource?.token.isCancellationRequested) { + return; + } await utils.sleep(300); } } @@ -299,7 +352,7 @@ export class SessionManager implements Middleware { if (codeLensToFix.command?.command === "editor.action.showReferences") { const oldArgs = codeLensToFix.command.arguments; if (oldArgs === undefined || oldArgs.length < 3) { - this.logger.writeError("Code Lens arguments were malformed"); + this.logger.writeError("Code Lens arguments were malformed!"); return codeLensToFix; } @@ -345,7 +398,9 @@ export class SessionManager implements Middleware { return resolvedCodeLens; } - // Move old setting codeFormatting.whitespaceAroundPipe to new setting codeFormatting.addWhitespaceAroundPipe + // TODO: Remove this migration code. Move old setting + // codeFormatting.whitespaceAroundPipe to new setting + // codeFormatting.addWhitespaceAroundPipe. private async migrateWhitespaceAroundPipeSetting(): Promise { const configuration = vscode.workspace.getConfiguration(utils.PowerShellLanguageId); const deprecatedSetting = "codeFormatting.whitespaceAroundPipe"; @@ -353,6 +408,7 @@ export class SessionManager implements Middleware { const configurationTargetOfNewSetting = getEffectiveConfigurationTarget(newSetting); const configurationTargetOfOldSetting = getEffectiveConfigurationTarget(deprecatedSetting); if (configurationTargetOfOldSetting !== undefined && configurationTargetOfNewSetting === undefined) { + this.logger.writeWarning("Deprecated setting: whitespaceAroundPipe"); const value = configuration.get(deprecatedSetting, configurationTargetOfOldSetting); await changeSetting(newSetting, value, configurationTargetOfOldSetting, this.logger); await changeSetting(deprecatedSetting, undefined, configurationTargetOfOldSetting, this.logger); @@ -365,6 +421,7 @@ export class SessionManager implements Middleware { return; } + this.logger.writeWarning("Deprecated setting: powerShellExePath"); let warningMessage = "The 'powerShell.powerShellExePath' setting is no longer used. "; warningMessage += this.sessionSettings.powerShellDefaultVersion ? "We can automatically remove it for you." @@ -394,14 +451,17 @@ export class SessionManager implements Middleware { const settings = getSettings(); this.logger.updateLogLevel(settings.developer.editorServicesLogLevel); - // Detect any setting changes that would affect the session + // Detect any setting changes that would affect the session. if (!this.suppressRestartPrompt && (settings.cwd.toLowerCase() !== this.sessionSettings.cwd.toLowerCase() || settings.powerShellDefaultVersion.toLowerCase() !== this.sessionSettings.powerShellDefaultVersion.toLowerCase() || settings.developer.editorServicesLogLevel.toLowerCase() !== this.sessionSettings.developer.editorServicesLogLevel.toLowerCase() || settings.developer.bundledModulesPath.toLowerCase() !== this.sessionSettings.developer.bundledModulesPath.toLowerCase() + || settings.developer.editorServicesWaitForDebugger !== this.sessionSettings.developer.editorServicesWaitForDebugger || settings.integratedConsole.useLegacyReadLine !== this.sessionSettings.integratedConsole.useLegacyReadLine || settings.integratedConsole.startInBackground !== this.sessionSettings.integratedConsole.startInBackground)) { + + this.logger.writeVerbose("Settings changed, prompting to restart..."); const response = await vscode.window.showInformationMessage( "The PowerShell runtime configuration has changed, would you like to start a new session?", "Yes", "No"); @@ -427,87 +487,8 @@ export class SessionManager implements Middleware { ]; } - private async startPowerShell(): Promise { - if (this.PowerShellExeDetails === undefined) { - void this.setSessionFailedGetPowerShell("Unable to find PowerShell, try installing it?"); - return; - } - - this.setSessionStatus("Starting...", SessionStatus.Initializing); - - const bundledModulesPath = await this.getBundledModulesPath(); - const languageServerProcess = - new PowerShellProcess( - this.PowerShellExeDetails.exePath, - bundledModulesPath, - "PowerShell Extension", - this.logger, - this.buildEditorServicesArgs(bundledModulesPath, this.PowerShellExeDetails), - this.getNewSessionFilePath(), - this.sessionSettings); - - languageServerProcess.onExited( - async () => { - if (this.sessionStatus === SessionStatus.Running) { - this.setSessionStatus("Session Exited!", SessionStatus.Failed); - await this.promptForRestart(); - } - }); - - try { - this.sessionDetails = await languageServerProcess.start("EditorServices"); - } catch (err) { - // PowerShell never started, probably a bad version! - const version = await languageServerProcess.getVersionCli(); - let shouldUpdate = true; - if (satisfies(version, "<5.1.0")) { - void this.setSessionFailedGetPowerShell(`PowerShell ${version} is not supported, please update!`); - } else if (satisfies(version, ">=5.1.0 <6.0.0")) { - void this.setSessionFailedGetPowerShell("It looks like you're trying to use Windows PowerShell, which is supported on a best-effort basis. Can you try PowerShell 7?"); - } else if (satisfies(version, ">=6.0.0 <7.2.0")) { - void this.setSessionFailedGetPowerShell(`PowerShell ${version} has reached end-of-support, please update!`); - } else { - shouldUpdate = false; - void this.setSessionFailedOpenBug("PowerShell language server process didn't start!"); - } - if (shouldUpdate) { - // Run the update notifier since it won't run later as we failed - // to start, but we have enough details to do so now. - const versionDetails: IPowerShellVersionDetails = { - "version": version, - "edition": "", // Unused by UpdatePowerShell - "commit": version, // Actually used by UpdatePowerShell - "architecture": process.arch // Best guess based off Code's architecture - }; - const updater = new UpdatePowerShell(this.sessionSettings, this.logger, versionDetails); - void updater.checkForUpdate(); - } - return; - } - - if (this.sessionDetails.status === "started") { // Successful server start with a session file - this.logger.write("Language server started."); - try { - await this.startLanguageClient(this.sessionDetails); - return languageServerProcess; - } catch (err) { - void this.setSessionFailedOpenBug("Language client failed to start: " + (err instanceof Error ? err.message : "unknown")); - } - } else if (this.sessionDetails.status === "failed") { // Server started but indicated it failed - if (this.sessionDetails.reason === "powerShellVersion") { - void this.setSessionFailedGetPowerShell(`PowerShell ${this.sessionDetails.powerShellVersion} is not supported, please update!`); - } else if (this.sessionDetails.reason === "dotNetVersion") { // Only applies to PowerShell 5.1 - void this.setSessionFailedGetDotNet(".NET Framework is out-of-date, please install at least 4.8!"); - } else { - void this.setSessionFailedOpenBug(`PowerShell could not be started for an unknown reason: ${this.sessionDetails.reason}`); - } - } else { - void this.setSessionFailedOpenBug(`PowerShell could not be started with an unknown status: ${this.sessionDetails.status}, and reason: ${this.sessionDetails.reason}`); - } - return; - } - private async findPowerShell(): Promise { + this.logger.writeVerbose("Finding PowerShell..."); const powershellExeFinder = new PowerShellExeFinder( this.platformDetails, this.sessionSettings.powerShellAdditionalExePaths, @@ -532,110 +513,94 @@ export class SessionManager implements Middleware { void this.logger.writeAndShowWarning(`The 'powerShellDefaultVersion' setting was '${wantedName}' but this was not found!` + ` Instead using first available installation '${foundPowerShell.displayName}' at '${foundPowerShell.exePath}'!`); } - } catch (e) { - this.logger.writeError(`Error occurred while searching for a PowerShell executable:\n${e}`); - } - - if (foundPowerShell === undefined) { - const message = "Unable to find PowerShell." - + " Do you have PowerShell installed?" - + " You can also configure custom PowerShell installations" - + " with the 'powershell.powerShellAdditionalExePaths' setting."; - void this.setSessionFailedGetPowerShell(message); + } catch (err) { + this.logger.writeError(`Error occurred while searching for a PowerShell executable:\n${err}`); } return foundPowerShell; } - private async getBundledModulesPath(): Promise { - // Because the extension is always at `/out/main.js` - let bundledModulesPath = path.resolve(__dirname, "../modules"); + private async startLanguageServerProcess( + powerShellExeDetails: IPowerShellExeDetails, + cancellationToken: vscode.CancellationToken): Promise { - if (this.extensionContext.extensionMode === vscode.ExtensionMode.Development) { - const devBundledModulesPath = path.resolve(__dirname, this.sessionSettings.developer.bundledModulesPath); + const bundledModulesPath = await this.getBundledModulesPath(); + const languageServerProcess = + new PowerShellProcess( + powerShellExeDetails.exePath, + bundledModulesPath, + "PowerShell Extension", + this.logger, + this.getEditorServicesArgs(bundledModulesPath, powerShellExeDetails), + this.getNewSessionFilePath(), + this.sessionSettings); - // Make sure the module's bin path exists - if (await utils.checkIfDirectoryExists(devBundledModulesPath)) { - bundledModulesPath = devBundledModulesPath; - } else { - void this.logger.writeAndShowWarning( - "In development mode but PowerShellEditorServices dev module path cannot be " + - `found (or has not been built yet): ${devBundledModulesPath}\n`); - } - } + languageServerProcess.onExited( + async () => { + if (this.sessionStatus === SessionStatus.Running + || this.sessionStatus === SessionStatus.Busy) { + this.setSessionStatus("Session Exited!", SessionStatus.Failed); + await this.promptForRestart(); + } + }); - return bundledModulesPath; + this.sessionDetails = await languageServerProcess.start("EditorServices", cancellationToken); + + return languageServerProcess; } - private buildEditorServicesArgs(bundledModulesPath: string, powerShellExeDetails: IPowerShellExeDetails): string { - let editorServicesArgs = - "-HostName 'Visual Studio Code Host' " + - "-HostProfileId 'Microsoft.VSCode' " + - `-HostVersion '${this.HostVersion}' ` + - "-AdditionalModules @('PowerShellEditorServices.VSCode') " + - `-BundledModulesPath '${utils.escapeSingleQuotes(bundledModulesPath)}' ` + - "-EnableConsoleRepl "; + // The process failed to start, so check for common user errors (generally + // out-of-support versions of PowerShell). + private async handleFailedProcess(powerShellProcess: PowerShellProcess): Promise { + const version = await powerShellProcess.getVersionCli(); + let shouldUpdate = true; - if (this.sessionSettings.integratedConsole.suppressStartupBanner) { - editorServicesArgs += "-StartupBanner '' "; - } else if (utils.isWindows && !powerShellExeDetails.supportsProperArguments) { - // NOTE: On Windows we don't Base64 encode the startup command - // because it annoys some poorly implemented anti-virus scanners. - // Unfortunately this means that for some installs of PowerShell - // (such as through the `dotnet` package manager), we can't include - // a multi-line startup banner as the quotes break the command. - editorServicesArgs += `-StartupBanner '${this.DisplayName} Extension v${this.HostVersion}' `; + if (satisfies(version, "<5.1.0")) { + void this.setSessionFailedGetPowerShell(`PowerShell v${version} is not supported, please update!`); + } else if (satisfies(version, ">=5.1.0 <6.0.0")) { + void this.setSessionFailedGetPowerShell("It looks like you're trying to use Windows PowerShell, which is supported on a best-effort basis. Can you try PowerShell 7?"); + } else if (satisfies(version, ">=6.0.0 <7.2.0")) { + void this.setSessionFailedGetPowerShell(`PowerShell v${version} has reached end-of-support, please update!`); } else { - const startupBanner = `${this.DisplayName} Extension v${this.HostVersion} -Copyright (c) Microsoft Corporation. - -https://aka.ms/vscode-powershell -Type 'help' to get help. -`; - editorServicesArgs += `-StartupBanner "${startupBanner}" `; - } - - // We guard this here too out of an abundance of precaution. - if (this.sessionSettings.developer.editorServicesWaitForDebugger - && this.extensionContext.extensionMode === vscode.ExtensionMode.Development) { - editorServicesArgs += "-WaitForDebugger "; + shouldUpdate = false; + void this.setSessionFailedOpenBug("PowerShell Language Server process didn't start!"); + } + + if (shouldUpdate) { + // Run the update notifier since it won't run later as we failed + // to start, but we have enough details to do so now. + const versionDetails: IPowerShellVersionDetails = { + "version": version, + "edition": "", // Unused by UpdatePowerShell + "commit": version, // Actually used by UpdatePowerShell + "architecture": process.arch // Best guess based off Code's architecture + }; + const updater = new UpdatePowerShell(this.sessionSettings, this.logger, versionDetails); + void updater.checkForUpdate(); } - - editorServicesArgs += `-LogLevel '${this.sessionSettings.developer.editorServicesLogLevel}' `; - - return editorServicesArgs; } - private async promptForRestart(): Promise { - await this.logger.writeAndShowErrorWithActions( - "The PowerShell Extension Terminal has stopped, would you like to restart it? IntelliSense and other features will not work without it!", - [ - { - prompt: "Yes", - action: async (): Promise => { await this.restartSession(); } - }, - { - prompt: "No", - action: undefined - } - ] - ); - } - - private sendTelemetryEvent( - eventName: string, - properties?: TelemetryEventProperties, - measures?: TelemetryEventMeasurements): void { - - if (this.extensionContext.extensionMode === vscode.ExtensionMode.Production) { - this.telemetryReporter.sendTelemetryEvent(eventName, properties, measures); + private sessionStarted(sessionDetails: IEditorServicesSessionDetails): boolean { + this.logger.writeVerbose(`Session details: ${JSON.stringify(sessionDetails, undefined, 2)}`); + if (sessionDetails.status === "started") { // Successful server start with a session file + return true; + } + if (sessionDetails.status === "failed") { // Server started but indicated it failed + if (sessionDetails.reason === "powerShellVersion") { + void this.setSessionFailedGetPowerShell(`PowerShell ${sessionDetails.powerShellVersion} is not supported, please update!`); + } else if (sessionDetails.reason === "dotNetVersion") { // Only applies to PowerShell 5.1 + void this.setSessionFailedGetDotNet(".NET Framework is out-of-date, please install at least 4.8!"); + } else { + void this.setSessionFailedOpenBug(`PowerShell could not be started for an unknown reason: ${sessionDetails.reason}`); + } + } else { + void this.setSessionFailedOpenBug(`PowerShell could not be started with an unknown status: ${sessionDetails.status}, and reason: ${sessionDetails.reason}`); } + return false; } - private async startLanguageClient(sessionDetails: IEditorServicesSessionDetails): Promise { - this.logger.write(`Connecting to language service on pipe: ${sessionDetails.languageServicePipeName}`); - this.logger.write("Session details: " + JSON.stringify(sessionDetails)); - + private async startLanguageClient(sessionDetails: IEditorServicesSessionDetails): Promise { + this.logger.writeVerbose("Connecting to language service..."); const connectFunc = (): Promise => { return new Promise( (resolve, _reject) => { @@ -643,7 +608,7 @@ Type 'help' to get help. socket.on( "connect", () => { - this.logger.write("Language service socket connected."); + this.logger.writeVerbose("Language service connected."); resolve({ writer: socket, reader: socket }); }); }); @@ -683,13 +648,13 @@ Type 'help' to get help. middleware: this, }; - this.languageClient = new LanguageClient("PowerShell Editor Services", connectFunc, clientOptions); + const languageClient = new LanguageClient("PowerShell Editor Services", connectFunc, clientOptions); // This enables handling Semantic Highlighting messages in PowerShell Editor Services // TODO: We should only turn this on in preview. - this.languageClient.registerProposedFeatures(); + languageClient.registerProposedFeatures(); - this.languageClient.onTelemetry((event) => { + languageClient.onTelemetry((event) => { const eventName: string = event.eventName ? event.eventName : "PSESEvent"; // eslint-disable-next-line @typescript-eslint/no-explicit-any const data: any = event.data ? event.data : event; @@ -700,7 +665,7 @@ Type 'help' to get help. // so that they can register their message handlers // before the connection is established. for (const consumer of this.languageClientConsumers) { - consumer.setLanguageClient(this.languageClient); + consumer.setLanguageClient(languageClient); } this.registeredHandlers = [ @@ -708,11 +673,11 @@ Type 'help' to get help. // Console.ReadKey, since it's not cancellable. On // "cancellation" the server asks us to send pretend to // press a key, thus mitigating all the quirk. - this.languageClient.onNotification( + languageClient.onNotification( SendKeyPressNotificationType, () => { this.languageServerProcess?.sendKeyPress(); }), - this.languageClient.onNotification( + languageClient.onNotification( ExecutionBusyStatusNotificationType, (isBusy: boolean) => { if (isBusy) { this.setSessionBusyStatus(); } @@ -722,22 +687,111 @@ Type 'help' to get help. ]; try { - await this.languageClient.start(); + await languageClient.start(); } catch (err) { - void this.setSessionFailedOpenBug("Could not start language service: " + (err instanceof Error ? err.message : "unknown")); - return; + void this.setSessionFailedOpenBug("Language client failed to start: " + (err instanceof Error ? err.message : "unknown")); + } + + return languageClient; + } + + private async getBundledModulesPath(): Promise { + // Because the extension is always at `/out/main.js` + let bundledModulesPath = path.resolve(__dirname, "../modules"); + + if (this.extensionContext.extensionMode === vscode.ExtensionMode.Development) { + const devBundledModulesPath = path.resolve(__dirname, this.sessionSettings.developer.bundledModulesPath); + + // Make sure the module's bin path exists + if (await utils.checkIfDirectoryExists(devBundledModulesPath)) { + bundledModulesPath = devBundledModulesPath; + } else { + void this.logger.writeAndShowWarning( + "In development mode but PowerShellEditorServices dev module path cannot be " + + `found (or has not been built yet): ${devBundledModulesPath}\n`); + } + } + + return bundledModulesPath; + } + + private getEditorServicesArgs(bundledModulesPath: string, powerShellExeDetails: IPowerShellExeDetails): string { + let editorServicesArgs = + "-HostName 'Visual Studio Code Host' " + + "-HostProfileId 'Microsoft.VSCode' " + + `-HostVersion '${this.HostVersion}' ` + + "-AdditionalModules @('PowerShellEditorServices.VSCode') " + + `-BundledModulesPath '${utils.escapeSingleQuotes(bundledModulesPath)}' ` + + "-EnableConsoleRepl "; + + if (this.sessionSettings.integratedConsole.suppressStartupBanner) { + editorServicesArgs += "-StartupBanner '' "; + } else if (utils.isWindows && !powerShellExeDetails.supportsProperArguments) { + // NOTE: On Windows we don't Base64 encode the startup command + // because it annoys some poorly implemented anti-virus scanners. + // Unfortunately this means that for some installs of PowerShell + // (such as through the `dotnet` package manager), we can't include + // a multi-line startup banner as the quotes break the command. + editorServicesArgs += `-StartupBanner '${this.DisplayName} Extension v${this.HostVersion}' `; + } else { + const startupBanner = `${this.DisplayName} Extension v${this.HostVersion} +Copyright (c) Microsoft Corporation. + +https://aka.ms/vscode-powershell +Type 'help' to get help. +`; + editorServicesArgs += `-StartupBanner "${startupBanner}" `; + } + + // We guard this here too out of an abundance of precaution. + if (this.sessionSettings.developer.editorServicesWaitForDebugger + && this.extensionContext.extensionMode === vscode.ExtensionMode.Development) { + editorServicesArgs += "-WaitForDebugger "; } - this.versionDetails = await this.languageClient.sendRequest(PowerShellVersionRequestType); - this.setSessionRunningStatus(); - this.sendTelemetryEvent("powershellVersionCheck", { powershellVersion: this.versionDetails.version }); + editorServicesArgs += `-LogLevel '${this.sessionSettings.developer.editorServicesLogLevel}' `; + + return editorServicesArgs; + } + + private async getVersionDetails(): Promise { + // Take one minute to get version details, otherwise cancel and fail. + const timeout = new vscode.CancellationTokenSource(); + setTimeout(() => { timeout.cancel(); }, 60 * 1000); + + const versionDetails = await this.languageClient?.sendRequest( + PowerShellVersionRequestType, timeout.token); + + this.sendTelemetryEvent("powershellVersionCheck", + { powershellVersion: versionDetails?.version ?? "unknown" }); + + return versionDetails; + } + + private async promptForRestart(): Promise { + await this.logger.writeAndShowErrorWithActions( + "The PowerShell Extension Terminal has stopped, would you like to restart it? IntelliSense and other features will not work without it!", + [ + { + prompt: "Yes", + action: async (): Promise => { await this.restartSession(); } + }, + { + prompt: "No", + action: undefined + } + ] + ); + } - // We haven't "started" until we're done getting the version information. - this.started = true; + private sendTelemetryEvent( + eventName: string, + properties?: TelemetryEventProperties, + measures?: TelemetryEventMeasurements): void { - const updater = new UpdatePowerShell(this.sessionSettings, this.logger, this.versionDetails); - // NOTE: We specifically don't want to wait for this. - void updater.checkForUpdate(); + if (this.extensionContext.extensionMode === vscode.ExtensionMode.Production) { + this.telemetryReporter.sendTelemetryEvent(eventName, properties, measures); + } } private createStatusBarItem(): vscode.LanguageStatusItem { @@ -749,26 +803,27 @@ Type 'help' to get help. return languageStatusItem; } - private setSessionStatus(statusText: string, status: SessionStatus): void { + private setSessionStatus(detail: string, status: SessionStatus): void { + this.logger.writeVerbose(`Session status changing from '${this.sessionStatus}' to '${status}'.`); this.sessionStatus = status; + this.languageStatusItem.text = "$(terminal-powershell)"; this.languageStatusItem.detail = "PowerShell"; if (this.versionDetails !== undefined) { const semver = new SemVer(this.versionDetails.version); - this.languageStatusItem.text = `$(terminal-powershell) ${semver.major}.${semver.minor}`; + this.languageStatusItem.text += ` ${semver.major}.${semver.minor}`; this.languageStatusItem.detail += ` ${this.versionDetails.commit} (${this.versionDetails.architecture.toLowerCase()})`; } else if (this.PowerShellExeDetails?.displayName) { // In case it didn't start. - this.languageStatusItem.text = `$(terminal-powershell) ${this.PowerShellExeDetails.displayName}`; - this.languageStatusItem.detail += `: ${this.PowerShellExeDetails.exePath}`; + this.languageStatusItem.text += ` ${this.PowerShellExeDetails.displayName}`; + this.languageStatusItem.detail += ` at '${this.PowerShellExeDetails.exePath}'`; } - if (statusText) { - this.languageStatusItem.detail += ": " + statusText; + if (detail) { + this.languageStatusItem.detail += ": " + detail; } switch (status) { case SessionStatus.Running: - case SessionStatus.NeverStarted: case SessionStatus.NotStarted: this.languageStatusItem.busy = false; this.languageStatusItem.severity = vscode.LanguageStatusSeverity.Information; @@ -777,7 +832,7 @@ Type 'help' to get help. this.languageStatusItem.busy = true; this.languageStatusItem.severity = vscode.LanguageStatusSeverity.Information; break; - case SessionStatus.Initializing: + case SessionStatus.Starting: case SessionStatus.Stopping: this.languageStatusItem.busy = true; this.languageStatusItem.severity = vscode.LanguageStatusSeverity.Warning; @@ -799,7 +854,7 @@ Type 'help' to get help. } private async setSessionFailedOpenBug(message: string): Promise { - this.setSessionStatus("Initialization Error!", SessionStatus.Failed); + this.setSessionStatus("Startup Error!", SessionStatus.Failed); await this.logger.writeAndShowErrorWithActions(message, [{ prompt: "Open an Issue", action: async (): Promise => { @@ -809,7 +864,7 @@ Type 'help' to get help. } private async setSessionFailedGetPowerShell(message: string): Promise { - this.setSessionStatus("Initialization Error!", SessionStatus.Failed); + this.setSessionStatus("Startup Error!", SessionStatus.Failed); await this.logger.writeAndShowErrorWithActions(message, [{ prompt: "Open PowerShell Install Documentation", action: async (): Promise => { @@ -820,7 +875,7 @@ Type 'help' to get help. } private async setSessionFailedGetDotNet(message: string): Promise { - this.setSessionStatus("Initialization Error!", SessionStatus.Failed); + this.setSessionStatus("Startup Error!", SessionStatus.Failed); await this.logger.writeAndShowErrorWithActions(message, [{ prompt: "Open .NET Framework Documentation", action: async (): Promise => { @@ -867,59 +922,24 @@ Type 'help' to get help. this.logger); const availablePowerShellExes = await powershellExeFinder.getAllAvailablePowerShellInstallations(); - let sessionText: string; - - switch (this.sessionStatus) { - case SessionStatus.Running: - case SessionStatus.Initializing: - case SessionStatus.NotStarted: - case SessionStatus.NeverStarted: - case SessionStatus.Stopping: - if (this.PowerShellExeDetails && this.versionDetails) { - const currentPowerShellExe = - availablePowerShellExes - .find((item) => item.displayName.toLowerCase() === this.PowerShellExeDetails!.displayName.toLowerCase()); - - const powerShellSessionName = - currentPowerShellExe ? - currentPowerShellExe.displayName : - `PowerShell ${this.versionDetails.version} ` + - `(${this.versionDetails.architecture.toLowerCase()}) ${this.versionDetails.edition} Edition ` + - `[${this.versionDetails.version}]`; - - sessionText = `Current session: ${powerShellSessionName}`; - } else { - sessionText = "Current session: Unknown"; - } - break; - - case SessionStatus.Failed: - sessionText = "Session initialization failed, click here to show PowerShell extension logs"; - break; - - default: - throw new TypeError("Not a valid value for the enum 'SessionStatus'"); - } - - const powerShellItems = - availablePowerShellExes - .filter((item) => item.displayName !== this.PowerShellExeDetails?.displayName) - .map((item) => { - return new SessionMenuItem( - `Switch to: ${item.displayName}`, - async () => { await this.changePowerShellDefaultVersion(item); }); - }); + const powerShellItems = availablePowerShellExes + .filter((item) => item.displayName !== this.PowerShellExeDetails?.displayName) + .map((item) => { + return new SessionMenuItem( + `Switch to: ${item.displayName}`, + async () => { await this.changePowerShellDefaultVersion(item); }); + }); const menuItems: SessionMenuItem[] = [ new SessionMenuItem( - sessionText, + `Current session: ${this.PowerShellExeDetails?.displayName ?? "Unknown"} (click to show logs)`, async () => { await vscode.commands.executeCommand("PowerShell.ShowLogs"); }), // Add all of the different PowerShell options ...powerShellItems, new SessionMenuItem( - "Restart Current Session", + "Restart current session", async () => { // We pass in the display name so we guarantee that the session // will be the same PowerShell. @@ -931,7 +951,7 @@ Type 'help' to get help. }), new SessionMenuItem( - "Open Session Logs Folder", + "Open session logs folder", async () => { await vscode.commands.executeCommand("PowerShell.OpenLogFolder"); }), new SessionMenuItem( diff --git a/src/settings.ts b/src/settings.ts index 8c1f962529..88dc0fd543 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -195,7 +195,7 @@ export async function changeSetting( configurationTarget: vscode.ConfigurationTarget | boolean | undefined, logger: ILogger | undefined): Promise { - logger?.writeDiagnostic(`Changing '${settingName}' at scope '${configurationTarget} to '${newValue}'`); + logger?.writeVerbose(`Changing '${settingName}' at scope '${configurationTarget}' to '${newValue}'.`); try { const configuration = vscode.workspace.getConfiguration(utils.PowerShellLanguageId); diff --git a/src/utils.ts b/src/utils.ts index 8422291b72..2f8448494c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -34,7 +34,6 @@ async function checkIfFileOrDirectoryExists(targetPath: string | vscode.Uri, typ : vscode.Uri.file(targetPath)); return (stat.type & type) !== 0; } catch { - // TODO: Maybe throw if it's not a FileNotFound exception. return false; } }