From a02cd34b5421b5d24a61fedef55dd0cd38fa2c1c Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Fri, 18 Oct 2024 16:27:30 -0700 Subject: [PATCH 01/22] Implement LogOutputChannel and move settings to UI --- src/extension.ts | 7 ++- src/logging.ts | 108 +++++++++++++++++++---------------------------- src/session.ts | 5 +-- src/utils.ts | 26 ++++++++++++ 4 files changed, 75 insertions(+), 71 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 0cb8f3f52e..f6c9abedc1 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -22,7 +22,7 @@ import { ShowHelpFeature } from "./features/ShowHelp"; import { SpecifyScriptArgsFeature } from "./features/DebugSession"; import { Logger } from "./logging"; import { SessionManager } from "./session"; -import { LogLevel, getSettings } from "./settings"; +import { getSettings } from "./settings"; import { PowerShellLanguageId } from "./utils"; import { LanguageClientConsumer } from "./languageClientConsumer"; @@ -43,9 +43,8 @@ const documentSelector: DocumentSelector = [ ]; export async function activate(context: vscode.ExtensionContext): Promise { - const logLevel = vscode.workspace.getConfiguration(`${PowerShellLanguageId}.developer`) - .get("editorServicesLogLevel", LogLevel.Normal); - logger = new Logger(logLevel, context.globalStorageUri); + // This is the output channel for the PowerShell extension + logger = new Logger(context.globalStorageUri); telemetryReporter = new TelemetryReporter(TELEMETRY_KEY); diff --git a/src/logging.ts b/src/logging.ts index a7176c08ef..65e3470325 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,26 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import utils = require("./utils"); import os = require("os"); -import vscode = require("vscode"); - -// NOTE: This is not a string enum because the order is used for comparison. -export enum LogLevel { - Diagnostic, - Verbose, - Normal, - Warning, - Error, - None, -} +import { sleep, checkIfFileExists } from "./utils"; +import { LogOutputChannel, Uri, Disposable, LogLevel, window, env, commands, workspace } from "vscode"; /** Interface for logging operations. New features should use this interface for the "type" of logger. * This will allow for easy mocking of the logger during unit tests. */ export interface ILogger { - logDirectoryPath: vscode.Uri; - updateLogLevel(logLevelName: string): void; + logDirectoryPath: Uri; write(message: string, ...additionalMessages: string[]): void; writeAndShowInformation(message: string, ...additionalMessages: string[]): Promise; writeDiagnostic(message: string, ...additionalMessages: string[]): void; @@ -35,37 +24,36 @@ export interface ILogger { } export class Logger implements ILogger { - public logDirectoryPath: vscode.Uri; // The folder for all the logs - private logLevel: LogLevel; - private commands: vscode.Disposable[]; - private logChannel: vscode.OutputChannel; - private logFilePath: vscode.Uri; // The client's logs + public logDirectoryPath: Uri; // The folder for all the logs + private commands: Disposable[]; + // Log output channel handles all the verbosity management so we don't have to. + private logChannel: LogOutputChannel; + private logFilePath: Uri; // The client's logs private logDirectoryCreated = false; private writingLog = false; + public get logLevel(): LogLevel { return this.logChannel.logLevel;} - constructor(logLevelName: string, globalStorageUri: vscode.Uri) { - this.logLevel = Logger.logLevelNameToValue(logLevelName); - this.logChannel = vscode.window.createOutputChannel("PowerShell Extension Logs"); + constructor(globalStorageUri: Uri, logChannel?: LogOutputChannel) { + this.logChannel = logChannel ?? window.createOutputChannel("PowerShell", {log: true}); // We have to override the scheme because it defaults to // 'vscode-userdata' which breaks UNC paths. - this.logDirectoryPath = vscode.Uri.joinPath( + this.logDirectoryPath = Uri.joinPath( globalStorageUri.with({ scheme: "file" }), "logs", - `${Math.floor(Date.now() / 1000)}-${vscode.env.sessionId}`); - this.logFilePath = vscode.Uri.joinPath(this.logDirectoryPath, "vscode-powershell.log"); + `${Math.floor(Date.now() / 1000)}-${env.sessionId}`); + this.logFilePath = Uri.joinPath(this.logDirectoryPath, "vscode-powershell.log"); // Early logging of the log paths for debugging. - if (LogLevel.Diagnostic >= this.logLevel) { - const uriMessage = Logger.timestampMessage(`Log file path: '${this.logFilePath}'`, LogLevel.Verbose); - this.logChannel.appendLine(uriMessage); + if (this.logLevel >= LogLevel.Trace) { + this.logChannel.trace(`Log directory: ${this.logDirectoryPath.fsPath}`); } this.commands = [ - vscode.commands.registerCommand( + commands.registerCommand( "PowerShell.ShowLogs", () => { this.showLogPanel(); }), - vscode.commands.registerCommand( + commands.registerCommand( "PowerShell.OpenLogFolder", async () => { await this.openLogFolder(); }), ]; @@ -89,24 +77,24 @@ export class Logger implements ILogger { } public write(message: string, ...additionalMessages: string[]): void { - this.writeAtLevel(LogLevel.Normal, message, ...additionalMessages); + this.writeAtLevel(LogLevel.Info, message, ...additionalMessages); } public async writeAndShowInformation(message: string, ...additionalMessages: string[]): Promise { this.write(message, ...additionalMessages); - const selection = await vscode.window.showInformationMessage(message, "Show Logs", "Okay"); + const selection = await window.showInformationMessage(message, "Show Logs", "Okay"); if (selection === "Show Logs") { this.showLogPanel(); } } public writeDiagnostic(message: string, ...additionalMessages: string[]): void { - this.writeAtLevel(LogLevel.Diagnostic, message, ...additionalMessages); + this.writeAtLevel(LogLevel.Trace, message, ...additionalMessages); } public writeVerbose(message: string, ...additionalMessages: string[]): void { - this.writeAtLevel(LogLevel.Verbose, message, ...additionalMessages); + this.writeAtLevel(LogLevel.Debug, message, ...additionalMessages); } public writeWarning(message: string, ...additionalMessages: string[]): void { @@ -116,7 +104,7 @@ export class Logger implements ILogger { public async writeAndShowWarning(message: string, ...additionalMessages: string[]): Promise { this.writeWarning(message, ...additionalMessages); - const selection = await vscode.window.showWarningMessage(message, "Show Logs"); + const selection = await window.showWarningMessage(message, "Show Logs"); if (selection !== undefined) { this.showLogPanel(); } @@ -129,7 +117,7 @@ export class Logger implements ILogger { public async writeAndShowError(message: string, ...additionalMessages: string[]): Promise { this.writeError(message, ...additionalMessages); - const choice = await vscode.window.showErrorMessage(message, "Show Logs"); + const choice = await window.showErrorMessage(message, "Show Logs"); if (choice !== undefined) { this.showLogPanel(); } @@ -147,7 +135,7 @@ export class Logger implements ILogger { const actionKeys: string[] = fullActions.map((action) => action.prompt); - const choice = await vscode.window.showErrorMessage(message, ...actionKeys); + const choice = await window.showErrorMessage(message, ...actionKeys); if (choice) { for (const action of fullActions) { if (choice === action.prompt && action.action !== undefined ) { @@ -158,23 +146,6 @@ export class Logger implements ILogger { } } - // TODO: Make the enum smarter about strings so this goes away. - private static logLevelNameToValue(logLevelName: string): LogLevel { - switch (logLevelName.trim().toLowerCase()) { - case "diagnostic": return LogLevel.Diagnostic; - case "verbose": return LogLevel.Verbose; - case "normal": return LogLevel.Normal; - case "warning": return LogLevel.Warning; - case "error": return LogLevel.Error; - case "none": return LogLevel.None; - default: return LogLevel.Normal; - } - } - - public updateLogLevel(logLevelName: string): void { - this.logLevel = Logger.logLevelNameToValue(logLevelName); - } - private showLogPanel(): void { this.logChannel.show(); } @@ -183,7 +154,7 @@ export class Logger implements ILogger { if (this.logDirectoryCreated) { // Open the folder in VS Code since there isn't an easy way to // open the folder in the platform's file browser - await vscode.commands.executeCommand("vscode.openFolder", this.logDirectoryPath, true); + await commands.executeCommand("openFolder", this.logDirectoryPath, true); } else { void this.writeAndShowError("Cannot open PowerShell log directory as it does not exist!"); } @@ -195,26 +166,35 @@ export class Logger implements ILogger { } // TODO: Should we await this function above? - private async writeLine(message: string, level: LogLevel = LogLevel.Normal): Promise { - const timestampedMessage = Logger.timestampMessage(message, level); - this.logChannel.appendLine(timestampedMessage); - if (this.logLevel !== LogLevel.None) { + private async writeLine(message: string, level: LogLevel = LogLevel.Info): Promise { + switch (level) { + case LogLevel.Trace: this.logChannel.trace(message); break; + case LogLevel.Debug: this.logChannel.debug(message); break; + case LogLevel.Info: this.logChannel.info(message); break; + case LogLevel.Warning: this.logChannel.warn(message); break; + case LogLevel.Error: this.logChannel.error(message); break; + default: this.logChannel.appendLine(message); break; + } + + if (this.logLevel !== LogLevel.Off) { // A simple lock because this function isn't re-entrant. while (this.writingLog) { - await utils.sleep(300); + await sleep(300); } try { this.writingLog = true; if (!this.logDirectoryCreated) { this.writeVerbose(`Creating log directory at: '${this.logDirectoryPath}'`); - await vscode.workspace.fs.createDirectory(this.logDirectoryPath); + await workspace.fs.createDirectory(this.logDirectoryPath); this.logDirectoryCreated = true; } let log = new Uint8Array(); - if (await utils.checkIfFileExists(this.logFilePath)) { - log = await vscode.workspace.fs.readFile(this.logFilePath); + if (await checkIfFileExists(this.logFilePath)) { + log = await workspace.fs.readFile(this.logFilePath); } - await vscode.workspace.fs.writeFile( + + const timestampedMessage = Logger.timestampMessage(message, level); + await workspace.fs.writeFile( this.logFilePath, Buffer.concat([log, Buffer.from(timestampedMessage)])); } catch (err) { diff --git a/src/session.ts b/src/session.ts index a5f4314845..f45dd87182 100644 --- a/src/session.ts +++ b/src/session.ts @@ -14,7 +14,7 @@ import utils = require("./utils"); import { CloseAction, CloseHandlerResult, DocumentSelector, ErrorAction, ErrorHandlerResult, LanguageClientOptions, Middleware, NotificationType, - RequestType0, ResolveCodeLensSignature, RevealOutputChannelOn + RequestType0, ResolveCodeLensSignature } from "vscode-languageclient"; import { LanguageClient, StreamInfo } from "vscode-languageclient/node"; @@ -454,7 +454,6 @@ export class SessionManager implements Middleware { private async onConfigurationUpdated(): Promise { const settings = getSettings(); const shellIntegrationEnabled = vscode.workspace.getConfiguration("terminal.integrated.shellIntegration").get("enabled"); - this.logger.updateLogLevel(settings.developer.editorServicesLogLevel); // Detect any setting changes that would affect the session. if (!this.suppressRestartPrompt @@ -656,9 +655,9 @@ export class SessionManager implements Middleware { }; }, }, - revealOutputChannelOn: RevealOutputChannelOn.Never, middleware: this, traceOutputChannel: vscode.window.createOutputChannel("PowerShell Trace - LSP", {log: true}), + outputChannel: vscode.window.createOutputChannel("PowerShell Editor Services", {log: true}), }; const languageClient = new LanguageClient("powershell", "PowerShell Editor Services Client", connectFunc, clientOptions); diff --git a/src/utils.ts b/src/utils.ts index 7ea266e9c7..e3a3a9d7b3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -86,6 +86,32 @@ export function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } +/** + * Invokes the specified action when a PowerShell setting changes + * @param setting a string representation of the setting you wish to evaluate + * @param action the action to take when the setting changes + * @param scope the scope in which the vscode setting should be evaluated. Defaults to + * @param workspace + * @param onDidChangeConfiguration + * @example onPowerShellSettingChange("settingName", (newValue) => console.log(newValue)); + * @returns a Disposable object that can be used to stop listening for changes + */ + +// Because we actually do use the constraint in the callback +// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters +export function onPowerShellSettingChange( + setting: string, + listener: (newValue: T | undefined) => void, + section: "powershell", + scope?: vscode.ConfigurationScope, +): vscode.Disposable { + return vscode.workspace.onDidChangeConfiguration(e => { + if (!e.affectsConfiguration(section, scope)) { return; } + const value = vscode.workspace.getConfiguration(section, scope).get(setting); + listener(value); + }); +} + export const isMacOS: boolean = process.platform === "darwin"; export const isWindows: boolean = process.platform === "win32"; export const isLinux: boolean = !isMacOS && !isWindows; From a315bc7ef65faee5423834640b417aaee82d9c7f Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Fri, 18 Oct 2024 16:28:08 -0700 Subject: [PATCH 02/22] Remove unnecessary comment --- src/extension.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index f6c9abedc1..f35b9dd239 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -43,7 +43,6 @@ const documentSelector: DocumentSelector = [ ]; export async function activate(context: vscode.ExtensionContext): Promise { - // This is the output channel for the PowerShell extension logger = new Logger(context.globalStorageUri); telemetryReporter = new TelemetryReporter(TELEMETRY_KEY); From 87d3e6aa6c20fed4e6d19149266934884e535b18 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Fri, 18 Oct 2024 16:45:20 -0700 Subject: [PATCH 03/22] Remove File Logging (done by LogOutputWindow automatically) --- src/extension.ts | 2 +- src/logging.ts | 77 ++++++++++-------------------------------------- 2 files changed, 17 insertions(+), 62 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index f35b9dd239..7c9bf4f89e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -43,7 +43,7 @@ const documentSelector: DocumentSelector = [ ]; export async function activate(context: vscode.ExtensionContext): Promise { - logger = new Logger(context.globalStorageUri); + logger = new Logger(context.logUri); telemetryReporter = new TelemetryReporter(TELEMETRY_KEY); diff --git a/src/logging.ts b/src/logging.ts index 65e3470325..05935a38ea 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,9 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import os = require("os"); -import { sleep, checkIfFileExists } from "./utils"; -import { LogOutputChannel, Uri, Disposable, LogLevel, window, env, commands, workspace } from "vscode"; +import { LogOutputChannel, Uri, Disposable, LogLevel, window, commands } from "vscode"; /** Interface for logging operations. New features should use this interface for the "type" of logger. * This will allow for easy mocking of the logger during unit tests. @@ -28,23 +26,16 @@ export class Logger implements ILogger { private commands: Disposable[]; // Log output channel handles all the verbosity management so we don't have to. private logChannel: LogOutputChannel; - private logFilePath: Uri; // The client's logs - private logDirectoryCreated = false; - private writingLog = false; public get logLevel(): LogLevel { return this.logChannel.logLevel;} - constructor(globalStorageUri: Uri, logChannel?: LogOutputChannel) { + constructor(logPath: Uri, logChannel?: LogOutputChannel) { this.logChannel = logChannel ?? window.createOutputChannel("PowerShell", {log: true}); // We have to override the scheme because it defaults to // 'vscode-userdata' which breaks UNC paths. - this.logDirectoryPath = Uri.joinPath( - globalStorageUri.with({ scheme: "file" }), - "logs", - `${Math.floor(Date.now() / 1000)}-${env.sessionId}`); - this.logFilePath = Uri.joinPath(this.logDirectoryPath, "vscode-powershell.log"); + this.logDirectoryPath = logPath; // Early logging of the log paths for debugging. - if (this.logLevel >= LogLevel.Trace) { + if (this.logLevel > LogLevel.Off) { this.logChannel.trace(`Log directory: ${this.logDirectoryPath.fsPath}`); } @@ -151,57 +142,21 @@ export class Logger implements ILogger { } private async openLogFolder(): Promise { - if (this.logDirectoryCreated) { - // Open the folder in VS Code since there isn't an easy way to - // open the folder in the platform's file browser - await commands.executeCommand("openFolder", this.logDirectoryPath, true); - } else { - void this.writeAndShowError("Cannot open PowerShell log directory as it does not exist!"); - } - } - - private static timestampMessage(message: string, level: LogLevel): string { - const now = new Date(); - return `${now.toLocaleDateString()} ${now.toLocaleTimeString()} [${LogLevel[level].toUpperCase()}] - ${message}${os.EOL}`; + await commands.executeCommand("openFolder", this.logDirectoryPath, true); } - // TODO: Should we await this function above? private async writeLine(message: string, level: LogLevel = LogLevel.Info): Promise { - switch (level) { - case LogLevel.Trace: this.logChannel.trace(message); break; - case LogLevel.Debug: this.logChannel.debug(message); break; - case LogLevel.Info: this.logChannel.info(message); break; - case LogLevel.Warning: this.logChannel.warn(message); break; - case LogLevel.Error: this.logChannel.error(message); break; - default: this.logChannel.appendLine(message); break; - } - - if (this.logLevel !== LogLevel.Off) { - // A simple lock because this function isn't re-entrant. - while (this.writingLog) { - await sleep(300); + return new Promise((resolve) => { + switch (level) { + case LogLevel.Off: break; + case LogLevel.Trace: this.logChannel.trace(message); break; + case LogLevel.Debug: this.logChannel.debug(message); break; + case LogLevel.Info: this.logChannel.info(message); break; + case LogLevel.Warning: this.logChannel.warn(message); break; + case LogLevel.Error: this.logChannel.error(message); break; + default: this.logChannel.appendLine(message); break; } - try { - this.writingLog = true; - if (!this.logDirectoryCreated) { - this.writeVerbose(`Creating log directory at: '${this.logDirectoryPath}'`); - await workspace.fs.createDirectory(this.logDirectoryPath); - this.logDirectoryCreated = true; - } - let log = new Uint8Array(); - if (await checkIfFileExists(this.logFilePath)) { - log = await workspace.fs.readFile(this.logFilePath); - } - - const timestampedMessage = Logger.timestampMessage(message, level); - await workspace.fs.writeFile( - this.logFilePath, - Buffer.concat([log, Buffer.from(timestampedMessage)])); - } catch (err) { - console.log(`Error writing to vscode-powershell log file: ${err}`); - } finally { - this.writingLog = false; - } - } + resolve(); + }); } } From ec583253cf98891a03cb78dab4a9e6d48fc3e638 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 29 Oct 2024 08:50:09 -0700 Subject: [PATCH 04/22] First Connect --- package.json | 4 ++-- src/features/DebugSession.ts | 32 ++++++++++++++++------------ src/session.ts | 25 ++++++++++++++++++++-- src/utils.ts | 41 +++++++++++++++++++++++++++++------- 4 files changed, 76 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 55b397dab6..68bd7f2c22 100644 --- a/package.json +++ b/package.json @@ -1011,12 +1011,12 @@ "verbose" ], "default": "off", - "markdownDescription": "Traces the communication between VS Code and the PowerShell Editor Services [LSP Server](https://microsoft.github.io/language-server-protocol/). **only for extension developers and issue troubleshooting!**" + "markdownDescription": "Traces the communication between VS Code and the PowerShell Editor Services [LSP Server](https://microsoft.github.io/language-server-protocol/). **Only for extension developers and issue troubleshooting!**" }, "powershell.trace.dap": { "type": "boolean", "default": false, - "markdownDescription": "Traces the communication between VS Code and the PowerShell Editor Services [DAP Server](https://microsoft.github.io/debug-adapter-protocol/). **This setting is only meant for extension developers and issue troubleshooting!**" + "markdownDescription": "Traces the communication between VS Code and the PowerShell Editor Services [DAP Server](https://microsoft.github.io/debug-adapter-protocol/). **Only meant for extension developers and issue troubleshooting!**" } } }, diff --git a/src/features/DebugSession.ts b/src/features/DebugSession.ts index f9e4feae07..ca39820aae 100644 --- a/src/features/DebugSession.ts +++ b/src/features/DebugSession.ts @@ -37,7 +37,7 @@ import { PowerShellProcess } from "../process"; import { IEditorServicesSessionDetails, SessionManager } from "../session"; import { getSettings } from "../settings"; import path from "path"; -import { checkIfFileExists } from "../utils"; +import { checkIfFileExists, onPowerShellSettingChange } from "../utils"; export const StartDebuggerNotificationType = new NotificationType("powerShell/startDebugger"); @@ -608,17 +608,9 @@ class PowerShellDebugAdapterTrackerFactory implements DebugAdapterTrackerFactory disposables: Disposable[] = []; dapLogEnabled: boolean = workspace.getConfiguration("powershell").get("trace.dap") ?? false; constructor(private adapterName = "PowerShell") { - this.disposables.push(workspace.onDidChangeConfiguration(change => { - if ( - change.affectsConfiguration("powershell.trace.dap") - ) { - this.dapLogEnabled = workspace.getConfiguration("powershell").get("trace.dap") ?? false; - if (this.dapLogEnabled) { - // Trigger the output pane to appear. This gives the user time to position it before starting a debug. - this.log?.show(true); - } - } - })); + this.disposables.push( + onPowerShellSettingChange("trace.dap", (visible:boolean | undefined) => {this.setLogVisibility(visible);}) + ); } /* We want to use a shared output log for separate debug sessions as usually only one is running at a time and we @@ -630,10 +622,20 @@ class PowerShellDebugAdapterTrackerFactory implements DebugAdapterTrackerFactory _log: LogOutputChannel | undefined; get log(): LogOutputChannel | undefined { if (this.dapLogEnabled && this._log === undefined) { - this._log = window.createOutputChannel(`${this.adapterName} Trace - DAP`, { log: true }); + this._log = window.createOutputChannel(`${this.adapterName}: Trace DAP`, { log: true }); this.disposables.push(this._log); } - return this.dapLogEnabled ? this._log : undefined; + return this._log; + } + + private setLogVisibility(visible?: boolean): void { + if (this.log !== undefined) { + if (visible) { + this.log.show(true); + } else { + this.log.hide(); + } + } } createDebugAdapterTracker(session: DebugSession): DebugAdapterTracker { @@ -663,6 +665,8 @@ class PowerShellDebugAdapterTrackerFactory implements DebugAdapterTrackerFactory }; } + + dispose(): void { this.disposables.forEach(d => d.dispose()); } diff --git a/src/session.ts b/src/session.ts index f45dd87182..d22aeebe6d 100644 --- a/src/session.ts +++ b/src/session.ts @@ -94,6 +94,20 @@ export class SessionManager implements Middleware { private suppressRestartPrompt = false; private versionDetails: IPowerShellVersionDetails | undefined; + private _outputChannel?: vscode.LogOutputChannel; + /** Omnisharp and PSES messages sent via LSP are surfaced here. */ + private get outputChannel(): vscode.LogOutputChannel { + return this._outputChannel + ??= vscode.window.createOutputChannel("PowerShell: Editor Services", { log: true }); + } + + private _traceOutputChannel?: vscode.LogOutputChannel; + /** The LanguageClient LSP message trace is surfaced here. */ + private get traceOutputChannel(): vscode.LogOutputChannel { + return this._traceOutputChannel + ??= vscode.window.createOutputChannel("PowerShell: Trace LSP", { log: true }); + } + constructor( private extensionContext: vscode.ExtensionContext, private sessionSettings: Settings, @@ -131,13 +145,16 @@ export class SessionManager implements Middleware { this.HostVersion = this.HostVersion.split("-")[0]; this.registerCommands(); + } + public async dispose(): Promise { await this.stop(); // A whole lot of disposals. this.languageStatusItem.dispose(); + for (const handler of this.registeredHandlers) { handler.dispose(); } @@ -622,6 +639,7 @@ export class SessionManager implements Middleware { }); }); }; + const clientOptions: LanguageClientOptions = { documentSelector: this.documentSelector, synchronize: { @@ -645,9 +663,11 @@ export class SessionManager implements Middleware { // hangs up (ECONNRESET errors). error: (_error: Error, _message: Message, _count: number): ErrorHandlerResult => { // TODO: Is there any error worth terminating on? + this.logger.writeError(`${_error.name}: ${_error.message} ${_error.cause}`); return { action: ErrorAction.Continue }; }, closed: (): CloseHandlerResult => { + this.logger.write("Language service connection closed."); // We have our own restart experience return { action: CloseAction.DoNotRestart, @@ -656,8 +676,8 @@ export class SessionManager implements Middleware { }, }, middleware: this, - traceOutputChannel: vscode.window.createOutputChannel("PowerShell Trace - LSP", {log: true}), - outputChannel: vscode.window.createOutputChannel("PowerShell Editor Services", {log: true}), + traceOutputChannel: this.traceOutputChannel, + outputChannel: this.outputChannel }; const languageClient = new LanguageClient("powershell", "PowerShell Editor Services Client", connectFunc, clientOptions); @@ -666,6 +686,7 @@ export class SessionManager implements Middleware { // TODO: We should only turn this on in preview. languageClient.registerProposedFeatures(); + // NOTE: We don't currently send any events from PSES, but may again in // the future so we're leaving this side wired up. languageClient.onTelemetry((event) => { diff --git a/src/utils.ts b/src/utils.ts index e3a3a9d7b3..9d83543419 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -87,10 +87,11 @@ export function sleep(ms: number): Promise { } /** - * Invokes the specified action when a PowerShell setting changes - * @param setting a string representation of the setting you wish to evaluate + * Invokes the specified action when a setting changes + * @param setting a string representation of the setting you wish to evaluate, e.g. `trace.server` * @param action the action to take when the setting changes - * @param scope the scope in which the vscode setting should be evaluated. Defaults to + * @param section the section of the vscode settings to evaluate. Defaults to `powershell` + * @param scope the scope in which the vscode setting should be evaluated. * @param workspace * @param onDidChangeConfiguration * @example onPowerShellSettingChange("settingName", (newValue) => console.log(newValue)); @@ -99,19 +100,43 @@ export function sleep(ms: number): Promise { // Because we actually do use the constraint in the callback // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -export function onPowerShellSettingChange( +export function onSettingChange( + section: string, setting: string, - listener: (newValue: T | undefined) => void, - section: "powershell", + action: (newValue: T | undefined) => void, scope?: vscode.ConfigurationScope, ): vscode.Disposable { + const settingPath = `${section}.${setting}`; return vscode.workspace.onDidChangeConfiguration(e => { - if (!e.affectsConfiguration(section, scope)) { return; } + if (!e.affectsConfiguration(settingPath, scope)) { return; } const value = vscode.workspace.getConfiguration(section, scope).get(setting); - listener(value); + action(value); }); } +/** + * Invokes the specified action when a PowerShell setting changes. Convenience function for `onSettingChange` + * @param setting a string representation of the setting you wish to evaluate, e.g. `trace.server` + * @param action the action to take when the setting changes + * @param section the section of the vscode settings to evaluate. Defaults to `powershell` + * @param scope the scope in which the vscode setting should be evaluated. + * @param workspace + * @param onDidChangeConfiguration + * @example onPowerShellSettingChange("settingName", (newValue) => console.log(newValue)); + * @returns a Disposable object that can be used to stop listening for changes + */ + +// Because we actually do use the constraint in the callback +// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters +export function onPowerShellSettingChange( + setting: string, + action: (newValue: T | undefined) => void, + scope?: vscode.ConfigurationScope, +): vscode.Disposable { + const section = "powershell"; + return onSettingChange(section, setting, action, scope); +} + export const isMacOS: boolean = process.platform === "darwin"; export const isWindows: boolean = process.platform === "win32"; export const isLinux: boolean = !isMacOS && !isWindows; From 237a2f180c02438617ef633cb2d78fbcd9190566 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Wed, 30 Oct 2024 23:02:07 -0700 Subject: [PATCH 05/22] Add output adapters, LSP restart settings --- package.json | 36 ++++---- src/features/DebugSession.ts | 34 +++----- src/logging.ts | 158 ++++++++++++++++++++++++++++++++++- src/session.ts | 84 ++++++++++++------- src/settings.ts | 81 ++++++++++++++++++ src/utils.ts | 51 ----------- 6 files changed, 325 insertions(+), 119 deletions(-) diff --git a/package.json b/package.json index 68bd7f2c22..4ffb9a4231 100644 --- a/package.json +++ b/package.json @@ -953,6 +953,27 @@ "default": [], "markdownDescription": "An array of strings that enable experimental features in the PowerShell extension. **No flags are currently available!**" }, + "powershell.developer.traceLsp": { + "type": "boolean", + "default": false, + "markdownDescription": "Traces the LSP communication between VS Code and the PowerShell Editor Services [LSP Server](https://microsoft.github.io/language-server-protocol/). The output will be logged and also visible in the Output pane, where the verbosity is configurable. **For extension developers and issue troubleshooting only!**" + }, + "powershell.developer.traceDap": { + "type": "boolean", + "default": false, + "markdownDescription": "Traces the DAP communication between VS Code and the PowerShell Editor Services [DAP Server](https://microsoft.github.io/debug-adapter-protocol/). The output will be logged and also visible in the Output pane, where the verbosity is configurable. **For extension developers and issue troubleshooting only!**" + }, + "powershell.trace.server": { + "type": "string", + "enum": [ + "off", + "messages", + "verbose" + ], + "default": "off", + "markdownDescription": "Traces the communication between VS Code and the PowerShell Editor Services [LSP Server](https://microsoft.github.io/language-server-protocol/). The output will be logged and also visible in the Output pane, where the verbosity is configurable. **For extension developers and issue troubleshooting only!**", + "markdownDeprecationMessage": "Please use the `powershell.developer.traceLsp` setting instead and control verbosity via the Output Window settings." + }, "powershell.developer.waitForSessionFileTimeoutSeconds": { "type": "number", "default": 240, @@ -1002,21 +1023,6 @@ "type": "boolean", "default": false, "markdownDescription": "Show buttons in the editor's title bar for moving the terminals pane (with the PowerShell Extension Terminal) around." - }, - "powershell.trace.server": { - "type": "string", - "enum": [ - "off", - "messages", - "verbose" - ], - "default": "off", - "markdownDescription": "Traces the communication between VS Code and the PowerShell Editor Services [LSP Server](https://microsoft.github.io/language-server-protocol/). **Only for extension developers and issue troubleshooting!**" - }, - "powershell.trace.dap": { - "type": "boolean", - "default": false, - "markdownDescription": "Traces the communication between VS Code and the PowerShell Editor Services [DAP Server](https://microsoft.github.io/debug-adapter-protocol/). **Only meant for extension developers and issue troubleshooting!**" } } }, diff --git a/src/features/DebugSession.ts b/src/features/DebugSession.ts index ca39820aae..84bd573f42 100644 --- a/src/features/DebugSession.ts +++ b/src/features/DebugSession.ts @@ -37,7 +37,7 @@ import { PowerShellProcess } from "../process"; import { IEditorServicesSessionDetails, SessionManager } from "../session"; import { getSettings } from "../settings"; import path from "path"; -import { checkIfFileExists, onPowerShellSettingChange } from "../utils"; +import { checkIfFileExists } from "../utils"; export const StartDebuggerNotificationType = new NotificationType("powerShell/startDebugger"); @@ -606,38 +606,26 @@ export class DebugSessionFeature extends LanguageClientConsumer class PowerShellDebugAdapterTrackerFactory implements DebugAdapterTrackerFactory, Disposable { disposables: Disposable[] = []; - dapLogEnabled: boolean = workspace.getConfiguration("powershell").get("trace.dap") ?? false; - constructor(private adapterName = "PowerShell") { - this.disposables.push( - onPowerShellSettingChange("trace.dap", (visible:boolean | undefined) => {this.setLogVisibility(visible);}) - ); - } + constructor(private adapterName = "PowerShell") {} + - /* We want to use a shared output log for separate debug sessions as usually only one is running at a time and we - * dont need an output window for every debug session. We also want to leave it active so user can copy and paste - * even on run end. When user changes the setting and disables it getter will return undefined, which will result + _log: LogOutputChannel | undefined; + /** Lazily creates a {@link LogOutputChannel} for debug tracing, and presents it only when DAP logging is enabled. + * + * We want to use a shared output log for separate debug sessions as usually only one is running at a time and we + * dont need an output window for every debug session. We also want to leave it active so user can copy and paste + * even on run end. When user changes the setting and disables it getter will return undefined, which will result * in a noop for the logging activities, effectively pausing logging but not disposing the output channel. If the * user re-enables, then logging resumes. */ - _log: LogOutputChannel | undefined; get log(): LogOutputChannel | undefined { - if (this.dapLogEnabled && this._log === undefined) { + if (workspace.getConfiguration("powershell.developer").get("traceDap") && this._log === undefined) { this._log = window.createOutputChannel(`${this.adapterName}: Trace DAP`, { log: true }); this.disposables.push(this._log); } return this._log; } - private setLogVisibility(visible?: boolean): void { - if (this.log !== undefined) { - if (visible) { - this.log.show(true); - } else { - this.log.hide(); - } - } - } - createDebugAdapterTracker(session: DebugSession): DebugAdapterTracker { const sessionInfo = `${this.adapterName} Debug Session: ${session.name} [${session.id}]`; return { @@ -665,8 +653,6 @@ class PowerShellDebugAdapterTrackerFactory implements DebugAdapterTrackerFactory }; } - - dispose(): void { this.disposables.forEach(d => d.dispose()); } diff --git a/src/logging.ts b/src/logging.ts index 05935a38ea..039a29e99f 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { LogOutputChannel, Uri, Disposable, LogLevel, window, commands } from "vscode"; +import { LogOutputChannel, Uri, Disposable, LogLevel, window, commands, Event } from "vscode"; /** Interface for logging operations. New features should use this interface for the "type" of logger. * This will allow for easy mocking of the logger during unit tests. @@ -160,3 +160,159 @@ export class Logger implements ILogger { }); } } + +/** Parses logs received via the legacy OutputChannel to LogOutputChannel with proper severity. + * + * HACK: This is for legacy compatability and can be removed when https://github.com/microsoft/vscode-languageserver-node/issues/1116 is merged and replaced with a normal LogOutputChannel. We don't use a middleware here because any direct logging calls like client.warn() would not be captured by middleware. + */ +export class LanguageClientOutputChannelAdapter implements LogOutputChannel { + private channel: LogOutputChannel; + constructor(channel: LogOutputChannel) { + this.channel = channel; + } + + public appendLine(message: string): void { + this.append(message); + } + + public append(message: string): void { + const [parsedMessage, level] = this.parse(message); + this.sendLogMessage(parsedMessage, level); + } + + protected parse(message: string): [string, LogLevel] { + const logLevelMatch = /^\[(?Trace|Debug|Info|Warn|Error) +- \d+:\d+:\d+ [AP]M\] (?.+)/.exec(message); + if (logLevelMatch) { + const { level, message } = logLevelMatch.groups!; + let logLevel: LogLevel; + switch (level) { + case "Trace": + logLevel = LogLevel.Trace; + break; + case "Debug": + logLevel = LogLevel.Debug; + break; + case "Info": + logLevel = LogLevel.Info; + break; + case "Warn": + logLevel = LogLevel.Warning; + break; + case "Error": + logLevel = LogLevel.Error; + break; + default: + logLevel = LogLevel.Info; + break; + } + return [message, logLevel]; + } else { + return [message, LogLevel.Info]; + } + } + + protected sendLogMessage(message: string, level: LogLevel): void { + switch (level) { + case LogLevel.Trace: + this.channel.trace(message); + break; + case LogLevel.Debug: + this.channel.debug(message); + break; + case LogLevel.Info: + this.channel.info(message); + break; + case LogLevel.Warning: + this.channel.warn(message); + break; + case LogLevel.Error: + this.channel.error(message); + break; + default: + this.channel.trace(message); + break; + } + } + + // #region Passthru Implementation + public get name(): string { + return this.channel.name; + } + public get logLevel(): LogLevel { + return this.channel.logLevel; + } + replace(value: string): void { + this.channel.replace(value); + } + show(_column?: undefined, preserveFocus?: boolean): void { + this.channel.show(preserveFocus); + } + public get onDidChangeLogLevel(): Event { + return this.channel.onDidChangeLogLevel; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public trace(message: string, ...args: any[]): void { + this.channel.trace(message, ...args); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public debug(message: string, ...args: any[]): void { + this.channel.debug(message, ...args); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public info(message: string, ...args: any[]): void { + this.channel.info(message, ...args); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public warn(message: string, ...args: any[]): void { + this.channel.warn(message, ...args); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public error(message: string, ...args: any[]): void { + this.channel.error(message, ...args); + } + public clear(): void { + this.channel.clear(); + } + public hide(): void { + this.channel.hide(); + } + public dispose(): void { + this.channel.dispose(); + } + // #endregion +} + +/** Overrides the severity of some LSP traces to be more logical */ +export class LanguageClientTraceFormatter extends LanguageClientOutputChannelAdapter { + + public override appendLine(message: string): void { + this.append(message); + } + + public override append(message: string): void { + // eslint-disable-next-line prefer-const + let [parsedMessage, level] = this.parse(message); + + // Override some LSP traces to be more logical + if (parsedMessage.startsWith("Sending ")) { + parsedMessage = parsedMessage.replace("Sending", "▶️"); + level = LogLevel.Debug; + } + if (parsedMessage.startsWith("Received ")) { + parsedMessage = parsedMessage.replace("Received", "◀️"); + level = LogLevel.Debug; + } + if (parsedMessage.startsWith("Params:") + || parsedMessage.startsWith("Result:") + ) { + level = LogLevel.Trace; + } + + // These are PSES messages we don't really need to see so we drop these to trace + if (parsedMessage.startsWith("◀️ notification 'window/logMessage'")) { + level = LogLevel.Trace; + } + + this.sendLogMessage(parsedMessage.trimEnd(), level); + } +} diff --git a/src/session.ts b/src/session.ts index d22aeebe6d..166956ed3c 100644 --- a/src/session.ts +++ b/src/session.ts @@ -6,7 +6,7 @@ import path = require("path"); import vscode = require("vscode"); import TelemetryReporter, { TelemetryEventProperties, TelemetryEventMeasurements } from "@vscode/extension-telemetry"; import { Message } from "vscode-jsonrpc"; -import { ILogger } from "./logging"; +import { ILogger, LanguageClientOutputChannelAdapter, LanguageClientTraceFormatter } from "./logging"; import { PowerShellProcess } from "./process"; import { Settings, changeSetting, getSettings, getEffectiveConfigurationTarget, validateCwdSetting } from "./settings"; import utils = require("./utils"); @@ -96,16 +96,24 @@ export class SessionManager implements Middleware { private _outputChannel?: vscode.LogOutputChannel; /** Omnisharp and PSES messages sent via LSP are surfaced here. */ - private get outputChannel(): vscode.LogOutputChannel { - return this._outputChannel - ??= vscode.window.createOutputChannel("PowerShell: Editor Services", { log: true }); + private get outputChannel(): vscode.LogOutputChannel | undefined { + return vscode.workspace.getConfiguration("powershell.developer").get("traceLsp", false) + ? this._outputChannel + ??= new LanguageClientOutputChannelAdapter( + vscode.window.createOutputChannel("PowerShell: Editor Services", { log: true }) + ) + : undefined; } private _traceOutputChannel?: vscode.LogOutputChannel; /** The LanguageClient LSP message trace is surfaced here. */ - private get traceOutputChannel(): vscode.LogOutputChannel { - return this._traceOutputChannel - ??= vscode.window.createOutputChannel("PowerShell: Trace LSP", { log: true }); + private get traceOutputChannel(): vscode.LogOutputChannel | undefined { + return vscode.workspace.getConfiguration("powershell.developer").get("traceLsp", false) + ? this._traceOutputChannel + ??= new LanguageClientTraceFormatter( + vscode.window.createOutputChannel("PowerShell: Trace LSP", { log: true }) + ) + : undefined; } constructor( @@ -145,16 +153,13 @@ export class SessionManager implements Middleware { this.HostVersion = this.HostVersion.split("-")[0]; this.registerCommands(); - } - public async dispose(): Promise { await this.stop(); // A whole lot of disposals. this.languageStatusItem.dispose(); - for (const handler of this.registeredHandlers) { handler.dispose(); } @@ -468,16 +473,37 @@ export class SessionManager implements Middleware { } } - private async onConfigurationUpdated(): Promise { + /** There are some changes we cannot "hot" set, so these require a restart of the session */ + private async restartOnCriticalConfigChange(changeEvent: vscode.ConfigurationChangeEvent): Promise { + if (this.suppressRestartPrompt) {return;} + if (this.sessionStatus !== SessionStatus.Running) {return;} + + // Restart not needed if shell integration is enabled but the shell is backgrounded. const settings = getSettings(); - const shellIntegrationEnabled = vscode.workspace.getConfiguration("terminal.integrated.shellIntegration").get("enabled"); + if (changeEvent.affectsConfiguration("terminal.integrated.shellIntegration.enabled")) { + const shellIntegrationEnabled = vscode.workspace.getConfiguration("terminal.integrated.shellIntegration").get("enabled") ?? false; + if (shellIntegrationEnabled && !settings.integratedConsole.startInBackground) { + return this.restartWithPrompt(); + } + } + + // Early return if the change doesn't affect the PowerShell extension settings from this point forward + if (!changeEvent.affectsConfiguration("powershell")) {return;} + // Detect any setting changes that would affect the session. - if (!this.suppressRestartPrompt - && this.sessionStatus === SessionStatus.Running - && ((shellIntegrationEnabled !== this.shellIntegrationEnabled - && !settings.integratedConsole.startInBackground) - || settings.cwd !== this.sessionSettings.cwd + const coldRestartSettingNames = [ + "developer.traceLsp", + "developer.traceDap" + ]; + for (const settingName of coldRestartSettingNames) { + if (changeEvent.affectsConfiguration("powershell" + "." + settingName)) { + return this.restartWithPrompt(); + } + } + + // TODO: Migrate these to affectsConfiguration style above + if (settings.cwd !== this.sessionSettings.cwd || settings.powerShellDefaultVersion !== this.sessionSettings.powerShellDefaultVersion || settings.developer.editorServicesLogLevel !== this.sessionSettings.developer.editorServicesLogLevel || settings.developer.bundledModulesPath !== this.sessionSettings.developer.bundledModulesPath @@ -485,16 +511,20 @@ export class SessionManager implements Middleware { || settings.developer.setExecutionPolicy !== this.sessionSettings.developer.setExecutionPolicy || settings.integratedConsole.useLegacyReadLine !== this.sessionSettings.integratedConsole.useLegacyReadLine || settings.integratedConsole.startInBackground !== this.sessionSettings.integratedConsole.startInBackground - || settings.integratedConsole.startLocation !== this.sessionSettings.integratedConsole.startLocation)) { + || settings.integratedConsole.startLocation !== this.sessionSettings.integratedConsole.startLocation + ) { + return this.restartWithPrompt(); + } + } - 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"); + private async restartWithPrompt(): Promise { + 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"); - if (response === "Yes") { - await this.restartSession(); - } + if (response === "Yes") { + await this.restartSession(); } } @@ -502,7 +532,7 @@ export class SessionManager implements Middleware { this.registeredCommands = [ vscode.commands.registerCommand("PowerShell.RestartSession", async () => { await this.restartSession(); }), vscode.commands.registerCommand(this.ShowSessionMenuCommandName, async () => { await this.showSessionMenu(); }), - vscode.workspace.onDidChangeConfiguration(async () => { await this.onConfigurationUpdated(); }), + vscode.workspace.onDidChangeConfiguration((e) => this.restartOnCriticalConfigChange(e)), vscode.commands.registerCommand( "PowerShell.ShowSessionConsole", (isExecute?: boolean) => { this.showSessionTerminal(isExecute); }) ]; @@ -639,7 +669,6 @@ export class SessionManager implements Middleware { }); }); }; - const clientOptions: LanguageClientOptions = { documentSelector: this.documentSelector, synchronize: { @@ -686,7 +715,6 @@ export class SessionManager implements Middleware { // TODO: We should only turn this on in preview. languageClient.registerProposedFeatures(); - // NOTE: We don't currently send any events from PSES, but may again in // the future so we're leaving this side wired up. languageClient.onTelemetry((event) => { diff --git a/src/settings.ts b/src/settings.ts index 9c2ef38452..f378b4ab68 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -316,3 +316,84 @@ export async function validateCwdSetting(logger: ILogger | undefined): Promise console.log(newValue)); + */ + +// Because we actually do use the constraint in the callback +// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters +export function onSettingChange( + section: string, + setting: string, + action: (newValue: T | undefined) => void, + options?: onSettingChangeOptions, +): vscode.Disposable { + const settingPath = `${section}.${setting}`; + const disposable = vscode.workspace.onDidChangeConfiguration(e => { + if (!e.affectsConfiguration(settingPath, options?.scope)) { return; } + + doOnSettingsChange(section, setting, action, options?.scope); + if (options?.run === "once") { + disposable.dispose(); // Javascript black magic, referring to an outer reference before it exists + } + }); + if (options?.run === "now") { + doOnSettingsChange(section, setting, action, options.scope); + } + return disposable; +} + +/** Implementation is separate to avoid duplicate code for run now */ + +// Because we actually do use the constraint in the callback +// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters +function doOnSettingsChange( + section: string, + setting: string, + action: (newValue: T | undefined) => void, + scope?: vscode.ConfigurationScope, +): void { + const value = vscode.workspace.getConfiguration(section, scope).get(setting); + action(value); +} + +/** + * Invokes the specified action when a PowerShell setting changes. Convenience function for `onSettingChange` + * @param setting a string representation of the setting you wish to evaluate, e.g. `trace.server` + * @param action the action to take when the setting changes + * @param scope the scope in which the vscode setting should be evaluated.n + * @returns a Disposable object that can be used to stop listening for changes + * @example + * onPowerShellSettingChange("settingName", (newValue) => console.log(newValue)); + */ + +// Because we actually do use the constraint in the callback +// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters +export function onPowerShellSettingChange( + setting: string, + action: (newValue: T | undefined) => void, + options?: onSettingChangeOptions + +): vscode.Disposable { + const section = "powershell"; + return onSettingChange(section, setting, action, options); +} diff --git a/src/utils.ts b/src/utils.ts index 9d83543419..7ea266e9c7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -86,57 +86,6 @@ export function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } -/** - * Invokes the specified action when a setting changes - * @param setting a string representation of the setting you wish to evaluate, e.g. `trace.server` - * @param action the action to take when the setting changes - * @param section the section of the vscode settings to evaluate. Defaults to `powershell` - * @param scope the scope in which the vscode setting should be evaluated. - * @param workspace - * @param onDidChangeConfiguration - * @example onPowerShellSettingChange("settingName", (newValue) => console.log(newValue)); - * @returns a Disposable object that can be used to stop listening for changes - */ - -// Because we actually do use the constraint in the callback -// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -export function onSettingChange( - section: string, - setting: string, - action: (newValue: T | undefined) => void, - scope?: vscode.ConfigurationScope, -): vscode.Disposable { - const settingPath = `${section}.${setting}`; - return vscode.workspace.onDidChangeConfiguration(e => { - if (!e.affectsConfiguration(settingPath, scope)) { return; } - const value = vscode.workspace.getConfiguration(section, scope).get(setting); - action(value); - }); -} - -/** - * Invokes the specified action when a PowerShell setting changes. Convenience function for `onSettingChange` - * @param setting a string representation of the setting you wish to evaluate, e.g. `trace.server` - * @param action the action to take when the setting changes - * @param section the section of the vscode settings to evaluate. Defaults to `powershell` - * @param scope the scope in which the vscode setting should be evaluated. - * @param workspace - * @param onDidChangeConfiguration - * @example onPowerShellSettingChange("settingName", (newValue) => console.log(newValue)); - * @returns a Disposable object that can be used to stop listening for changes - */ - -// Because we actually do use the constraint in the callback -// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -export function onPowerShellSettingChange( - setting: string, - action: (newValue: T | undefined) => void, - scope?: vscode.ConfigurationScope, -): vscode.Disposable { - const section = "powershell"; - return onSettingChange(section, setting, action, scope); -} - export const isMacOS: boolean = process.platform === "darwin"; export const isWindows: boolean = process.platform === "win32"; export const isLinux: boolean = !isMacOS && !isWindows; From 2b9a9279babf710320d4898393f5bb06cab3e49f Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 31 Oct 2024 21:44:10 -0700 Subject: [PATCH 06/22] Fix Log Uri Test --- src/features/ExternalApi.ts | 5 +++++ test/core/paths.test.ts | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/features/ExternalApi.ts b/src/features/ExternalApi.ts index 7943bf8fa6..4c19a94168 100644 --- a/src/features/ExternalApi.ts +++ b/src/features/ExternalApi.ts @@ -19,6 +19,7 @@ export interface IPowerShellExtensionClient { getPowerShellVersionDetails(uuid: string): Promise; waitUntilStarted(uuid: string): Promise; getStorageUri(): vscode.Uri; + getLogUri(): vscode.Uri; } /* @@ -171,6 +172,10 @@ export class ExternalApiFeature implements IPowerShellExtensionClient { return this.extensionContext.globalStorageUri.with({ scheme: "file"}); } + public getLogUri(): vscode.Uri { + return this.getLogUri().with({ scheme: "file"}); + } + public dispose(): void { // Nothing to dispose. } diff --git a/test/core/paths.test.ts b/test/core/paths.test.ts index 15f60f5bd1..703b22a53f 100644 --- a/test/core/paths.test.ts +++ b/test/core/paths.test.ts @@ -9,9 +9,11 @@ import { checkIfDirectoryExists, checkIfFileExists, ShellIntegrationScript } fro describe("Path assumptions", function () { let globalStorageUri: vscode.Uri; + let logUri: vscode.Uri; before(async () => { const extension: IPowerShellExtensionClient = await utils.ensureEditorServicesIsConnected(); globalStorageUri = extension.getStorageUri(); + logUri = extension.getLogUri(); }); it("Creates the session folder at the correct path", async function () { @@ -19,7 +21,7 @@ describe("Path assumptions", function () { }); it("Creates the log folder at the correct path", async function () { - assert(await checkIfDirectoryExists(vscode.Uri.joinPath(globalStorageUri, "logs"))); + assert(await checkIfDirectoryExists(logUri)); }); it("Finds the Terminal Shell Integration Script", async function () { From 184691051c30e53f34b31c3c941d4fdba7b28b67 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 31 Oct 2024 21:46:47 -0700 Subject: [PATCH 07/22] Forgot to add to extension facing API --- src/extension.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/extension.ts b/src/extension.ts index 7c9bf4f89e..ddc08ab1c7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -167,6 +167,7 @@ export async function activate(context: vscode.ExtensionContext): Promise externalApi.getPowerShellVersionDetails(uuid), waitUntilStarted: uuid => externalApi.waitUntilStarted(uuid), getStorageUri: () => externalApi.getStorageUri(), + getLogUri: () => externalApi.getLogUri(), }; } From 9b77ed37c26daeac7998ccdf8bb7eea96c37bb33 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 31 Oct 2024 21:49:54 -0700 Subject: [PATCH 08/22] Accidentally made a recursive rather than reference what I wanted to. Thanks Copilot... --- src/features/ExternalApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/ExternalApi.ts b/src/features/ExternalApi.ts index 4c19a94168..704fd5c65e 100644 --- a/src/features/ExternalApi.ts +++ b/src/features/ExternalApi.ts @@ -173,7 +173,7 @@ export class ExternalApiFeature implements IPowerShellExtensionClient { } public getLogUri(): vscode.Uri { - return this.getLogUri().with({ scheme: "file"}); + return this.extensionContext.logUri.with({ scheme: "file"}); } public dispose(): void { From 8b981265ce84c26785162d80378ddaefad6b87db Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Fri, 1 Nov 2024 16:53:16 -0700 Subject: [PATCH 09/22] Pre-Restart Experiments --- src/logging.ts | 45 ++++++++--------------------------------- src/process.ts | 3 ++- src/session.ts | 54 ++++++++++++++++++++++++-------------------------- 3 files changed, 36 insertions(+), 66 deletions(-) diff --git a/src/logging.ts b/src/logging.ts index 039a29e99f..9e140ab466 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { LogOutputChannel, Uri, Disposable, LogLevel, window, commands, Event } from "vscode"; +import { LogOutputChannel, LogLevel, window, Event } from "vscode"; /** Interface for logging operations. New features should use this interface for the "type" of logger. * This will allow for easy mocking of the logger during unit tests. */ export interface ILogger { - logDirectoryPath: Uri; write(message: string, ...additionalMessages: string[]): void; writeAndShowInformation(message: string, ...additionalMessages: string[]): Promise; writeDiagnostic(message: string, ...additionalMessages: string[]): void; @@ -22,39 +21,16 @@ export interface ILogger { } export class Logger implements ILogger { - public logDirectoryPath: Uri; // The folder for all the logs - private commands: Disposable[]; // Log output channel handles all the verbosity management so we don't have to. private logChannel: LogOutputChannel; public get logLevel(): LogLevel { return this.logChannel.logLevel;} - constructor(logPath: Uri, logChannel?: LogOutputChannel) { + constructor(logChannel?: LogOutputChannel) { this.logChannel = logChannel ?? window.createOutputChannel("PowerShell", {log: true}); - // We have to override the scheme because it defaults to - // 'vscode-userdata' which breaks UNC paths. - this.logDirectoryPath = logPath; - - // Early logging of the log paths for debugging. - if (this.logLevel > LogLevel.Off) { - this.logChannel.trace(`Log directory: ${this.logDirectoryPath.fsPath}`); - } - - this.commands = [ - commands.registerCommand( - "PowerShell.ShowLogs", - () => { this.showLogPanel(); }), - - commands.registerCommand( - "PowerShell.OpenLogFolder", - async () => { await this.openLogFolder(); }), - ]; } public dispose(): void { this.logChannel.dispose(); - for (const command of this.commands) { - command.dispose(); - } } private writeAtLevel(logLevel: LogLevel, message: string, ...additionalMessages: string[]): void { @@ -137,14 +113,10 @@ export class Logger implements ILogger { } } - private showLogPanel(): void { + public showLogPanel(): void { this.logChannel.show(); } - private async openLogFolder(): Promise { - await commands.executeCommand("openFolder", this.logDirectoryPath, true); - } - private async writeLine(message: string, level: LogLevel = LogLevel.Info): Promise { return new Promise((resolve) => { switch (level) { @@ -163,12 +135,13 @@ export class Logger implements ILogger { /** Parses logs received via the legacy OutputChannel to LogOutputChannel with proper severity. * - * HACK: This is for legacy compatability and can be removed when https://github.com/microsoft/vscode-languageserver-node/issues/1116 is merged and replaced with a normal LogOutputChannel. We don't use a middleware here because any direct logging calls like client.warn() would not be captured by middleware. + * HACK: This is for legacy compatability and can be removed when https://github.com/microsoft/vscode-languageserver-node/issues/1116 is merged and replaced with a normal LogOutputChannel. We don't use a middleware here because any direct logging calls like client.warn() and server-initiated messages would not be captured by middleware. */ export class LanguageClientOutputChannelAdapter implements LogOutputChannel { private channel: LogOutputChannel; - constructor(channel: LogOutputChannel) { - this.channel = channel; + + constructor(public channelName: string) { + this.channel = window.createOutputChannel(channelName, {log: true}); } public appendLine(message: string): void { @@ -229,7 +202,7 @@ export class LanguageClientOutputChannelAdapter implements LogOutputChannel { this.channel.error(message); break; default: - this.channel.trace(message); + this.channel.error("!UNKNOWN LOG LEVEL!: " + message); break; } } @@ -284,7 +257,6 @@ export class LanguageClientOutputChannelAdapter implements LogOutputChannel { /** Overrides the severity of some LSP traces to be more logical */ export class LanguageClientTraceFormatter extends LanguageClientOutputChannelAdapter { - public override appendLine(message: string): void { this.append(message); } @@ -293,7 +265,6 @@ export class LanguageClientTraceFormatter extends LanguageClientOutputChannelAda // eslint-disable-next-line prefer-const let [parsedMessage, level] = this.parse(message); - // Override some LSP traces to be more logical if (parsedMessage.startsWith("Sending ")) { parsedMessage = parsedMessage.replace("Sending", "▶️"); level = LogLevel.Debug; diff --git a/src/process.ts b/src/process.ts index d363c8e2ee..5e0d696321 100644 --- a/src/process.ts +++ b/src/process.ts @@ -30,6 +30,7 @@ export class PowerShellProcess { private isTemp: boolean, private shellIntegrationEnabled: boolean, private logger: ILogger, + private logDirectoryPath: vscode.Uri, private startPsesArgs: string, private sessionFilePath: vscode.Uri, private sessionSettings: Settings) { @@ -51,7 +52,7 @@ export class PowerShellProcess { : ""; this.startPsesArgs += - `-LogPath '${utils.escapeSingleQuotes(this.logger.logDirectoryPath.fsPath)}' ` + + `-LogPath '${utils.escapeSingleQuotes(this.logDirectoryPath.fsPath)}' ` + `-SessionDetailsPath '${utils.escapeSingleQuotes(this.sessionFilePath.fsPath)}' ` + `-FeatureFlags @(${featureFlags}) `; diff --git a/src/session.ts b/src/session.ts index 166956ed3c..71e3042758 100644 --- a/src/session.ts +++ b/src/session.ts @@ -5,8 +5,8 @@ import net = require("net"); import path = require("path"); import vscode = require("vscode"); import TelemetryReporter, { TelemetryEventProperties, TelemetryEventMeasurements } from "@vscode/extension-telemetry"; -import { Message } from "vscode-jsonrpc"; -import { ILogger, LanguageClientOutputChannelAdapter, LanguageClientTraceFormatter } from "./logging"; +import { Message, Trace } from "vscode-jsonrpc"; +import { ILogger } from "./logging"; import { PowerShellProcess } from "./process"; import { Settings, changeSetting, getSettings, getEffectiveConfigurationTarget, validateCwdSetting } from "./settings"; import utils = require("./utils"); @@ -14,7 +14,8 @@ import utils = require("./utils"); import { CloseAction, CloseHandlerResult, DocumentSelector, ErrorAction, ErrorHandlerResult, LanguageClientOptions, Middleware, NotificationType, - RequestType0, ResolveCodeLensSignature + RequestType0, ResolveCodeLensSignature, + RevealOutputChannelOn, } from "vscode-languageclient"; import { LanguageClient, StreamInfo } from "vscode-languageclient/node"; @@ -93,28 +94,7 @@ export class SessionManager implements Middleware { private startCancellationTokenSource: vscode.CancellationTokenSource | undefined; private suppressRestartPrompt = false; private versionDetails: IPowerShellVersionDetails | undefined; - - private _outputChannel?: vscode.LogOutputChannel; - /** Omnisharp and PSES messages sent via LSP are surfaced here. */ - private get outputChannel(): vscode.LogOutputChannel | undefined { - return vscode.workspace.getConfiguration("powershell.developer").get("traceLsp", false) - ? this._outputChannel - ??= new LanguageClientOutputChannelAdapter( - vscode.window.createOutputChannel("PowerShell: Editor Services", { log: true }) - ) - : undefined; - } - - private _traceOutputChannel?: vscode.LogOutputChannel; - /** The LanguageClient LSP message trace is surfaced here. */ - private get traceOutputChannel(): vscode.LogOutputChannel | undefined { - return vscode.workspace.getConfiguration("powershell.developer").get("traceLsp", false) - ? this._traceOutputChannel - ??= new LanguageClientTraceFormatter( - vscode.window.createOutputChannel("PowerShell: Trace LSP", { log: true }) - ) - : undefined; - } + private traceLogLevelHandler?: vscode.Disposable; constructor( private extensionContext: vscode.ExtensionContext, @@ -126,7 +106,6 @@ export class SessionManager implements Middleware { hostVersion: string, publisher: string, private telemetryReporter: TelemetryReporter) { - // Create the language status item this.languageStatusItem = this.createStatusBarItem(); // We have to override the scheme because it defaults to @@ -299,6 +278,8 @@ export class SessionManager implements Middleware { this.startCancellationTokenSource?.dispose(); this.startCancellationTokenSource = undefined; this.sessionDetails = undefined; + this.traceLogLevelHandler?.dispose(); + this.traceLogLevelHandler = undefined; this.setSessionStatus("Not Started", SessionStatus.NotStarted); } @@ -375,6 +356,7 @@ export class SessionManager implements Middleware { true, false, this.logger, + this.extensionContext.logUri, this.getEditorServicesArgs(bundledModulesPath, this.PowerShellExeDetails) + "-DebugServiceOnly ", this.getNewSessionFilePath(), this.sessionSettings); @@ -585,6 +567,7 @@ export class SessionManager implements Middleware { false, this.shellIntegrationEnabled, this.logger, + this.extensionContext.logUri, this.getEditorServicesArgs(bundledModulesPath, powerShellExeDetails), this.getNewSessionFilePath(), this.sessionSettings); @@ -669,6 +652,7 @@ export class SessionManager implements Middleware { }); }); }; + const clientOptions: LanguageClientOptions = { documentSelector: this.documentSelector, synchronize: { @@ -705,8 +689,9 @@ export class SessionManager implements Middleware { }, }, middleware: this, - traceOutputChannel: this.traceOutputChannel, - outputChannel: this.outputChannel + // traceOutputChannel: traceOutputChannel, + // outputChannel: outputChannel, + revealOutputChannelOn: RevealOutputChannelOn.Never }; const languageClient = new LanguageClient("powershell", "PowerShell Editor Services Client", connectFunc, clientOptions); @@ -759,6 +744,19 @@ export class SessionManager implements Middleware { return languageClient; } + /** Synchronizes a vscode LogOutputChannel log level to the LSP trace setting to minimize traffic */ + private async setLspTrace(languageClient: LanguageClient, level: vscode.LogLevel): Promise { + this.logger.writeVerbose("LSP Trace level changed to: " + level.toString()); + if (level == vscode.LogLevel.Trace) { + return languageClient.setTrace(Trace.Verbose); + } else if (level == vscode.LogLevel.Debug) { + return languageClient.setTrace(Trace.Messages); + } else { + return languageClient.setTrace(Trace.Off); + } + } + + private async getBundledModulesPath(): Promise { // Because the extension is always at `/out/main.js` let bundledModulesPath = path.resolve(__dirname, "../modules"); From f8e11f0a5ca47ccd542995f7f2a960c13bee32f2 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Fri, 1 Nov 2024 16:53:40 -0700 Subject: [PATCH 10/22] Move Commands out of logger temporarily --- src/extension.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/extension.ts b/src/extension.ts index ddc08ab1c7..88e74fdaa6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -139,6 +139,19 @@ export async function activate(context: vscode.ExtensionContext): Promise {await vscode.commands.executeCommand( + "vscode.openFolder", + context.logUri, + { forceNewWindow: true } + );} + ), + vscode.commands.registerCommand( + "PowerShell.ShowLogs", + () => {logger.showLogPanel();} + ) ]; const externalApi = new ExternalApiFeature(context, sessionManager, logger); From 15838c69ccfccff718682d1db6c443a0bb31f235 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Fri, 1 Nov 2024 19:44:03 -0700 Subject: [PATCH 11/22] Initial Cleanup of Logging, looks good ATM --- src/extension.ts | 2 +- src/session.ts | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 88e74fdaa6..df943bd32b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -43,7 +43,7 @@ const documentSelector: DocumentSelector = [ ]; export async function activate(context: vscode.ExtensionContext): Promise { - logger = new Logger(context.logUri); + logger = new Logger(); telemetryReporter = new TelemetryReporter(TELEMETRY_KEY); diff --git a/src/session.ts b/src/session.ts index 71e3042758..064f4e9421 100644 --- a/src/session.ts +++ b/src/session.ts @@ -6,7 +6,7 @@ import path = require("path"); import vscode = require("vscode"); import TelemetryReporter, { TelemetryEventProperties, TelemetryEventMeasurements } from "@vscode/extension-telemetry"; import { Message, Trace } from "vscode-jsonrpc"; -import { ILogger } from "./logging"; +import { ILogger, LanguageClientOutputChannelAdapter, LanguageClientTraceFormatter } from "./logging"; import { PowerShellProcess } from "./process"; import { Settings, changeSetting, getSettings, getEffectiveConfigurationTarget, validateCwdSetting } from "./settings"; import utils = require("./utils"); @@ -338,7 +338,6 @@ export class SessionManager implements Middleware { // handler when the process is disposed). this.debugSessionProcess?.dispose(); this.debugEventHandler?.dispose(); - if (this.PowerShellExeDetails === undefined) { return Promise.reject(new Error("Required PowerShellExeDetails undefined!")); } @@ -689,8 +688,8 @@ export class SessionManager implements Middleware { }, }, middleware: this, - // traceOutputChannel: traceOutputChannel, - // outputChannel: outputChannel, + traceOutputChannel: new LanguageClientTraceFormatter("PowerShell: Trace LSP"), + outputChannel: new LanguageClientOutputChannelAdapter("PowerShell: Editor Services"), revealOutputChannelOn: RevealOutputChannelOn.Never }; From 5b57a63a9b10a204bbfd21b382ba5c3dc338e8c5 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sat, 9 Nov 2024 12:32:15 -0700 Subject: [PATCH 12/22] Merge client and server editorservices logs --- src/session.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/session.ts b/src/session.ts index 064f4e9421..9053e4e74a 100644 --- a/src/session.ts +++ b/src/session.ts @@ -689,7 +689,8 @@ export class SessionManager implements Middleware { }, middleware: this, traceOutputChannel: new LanguageClientTraceFormatter("PowerShell: Trace LSP"), - outputChannel: new LanguageClientOutputChannelAdapter("PowerShell: Editor Services"), + // This is named the same as the Client log to merge the logs, but will be handled and disposed separately. + outputChannel: new LanguageClientOutputChannelAdapter("PowerShell"), revealOutputChannelOn: RevealOutputChannelOn.Never }; From bff22ed792af98255cc24910d65822104b7059f3 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sat, 9 Nov 2024 12:42:57 -0700 Subject: [PATCH 13/22] Add new MergedOutputChannel log --- src/logging.ts | 14 ++++++++++++++ src/session.ts | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/logging.ts b/src/logging.ts index 9e140ab466..385c424936 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -255,6 +255,20 @@ export class LanguageClientOutputChannelAdapter implements LogOutputChannel { // #endregion } +/** Appends additional */ +export class PsesMergedOutputChannel extends LanguageClientOutputChannelAdapter { + public override appendLine(message: string): void { + this.append(message); + } + + public override append(message: string): void { + const [parsedMessage, level] = this.parse(message); + + // Append PSES prefix to log messages to differentiate them from Client messages + this.sendLogMessage("[PSES] " + parsedMessage, level); + } +} + /** Overrides the severity of some LSP traces to be more logical */ export class LanguageClientTraceFormatter extends LanguageClientOutputChannelAdapter { public override appendLine(message: string): void { diff --git a/src/session.ts b/src/session.ts index 9053e4e74a..42385b36ca 100644 --- a/src/session.ts +++ b/src/session.ts @@ -6,7 +6,7 @@ import path = require("path"); import vscode = require("vscode"); import TelemetryReporter, { TelemetryEventProperties, TelemetryEventMeasurements } from "@vscode/extension-telemetry"; import { Message, Trace } from "vscode-jsonrpc"; -import { ILogger, LanguageClientOutputChannelAdapter, LanguageClientTraceFormatter } from "./logging"; +import { ILogger, LanguageClientOutputChannelAdapter, LanguageClientTraceFormatter, PsesMergedOutputChannel } from "./logging"; import { PowerShellProcess } from "./process"; import { Settings, changeSetting, getSettings, getEffectiveConfigurationTarget, validateCwdSetting } from "./settings"; import utils = require("./utils"); @@ -690,7 +690,7 @@ export class SessionManager implements Middleware { middleware: this, traceOutputChannel: new LanguageClientTraceFormatter("PowerShell: Trace LSP"), // This is named the same as the Client log to merge the logs, but will be handled and disposed separately. - outputChannel: new LanguageClientOutputChannelAdapter("PowerShell"), + outputChannel: new PsesMergedOutputChannel("PowerShell"), revealOutputChannelOn: RevealOutputChannelOn.Never }; From cb3f2f2f79b2a8bc11bbcec58d5e4207141a69fe Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sat, 9 Nov 2024 15:02:38 -0700 Subject: [PATCH 14/22] Remove unnecessary Import --- src/session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/session.ts b/src/session.ts index 42385b36ca..c7b8afa043 100644 --- a/src/session.ts +++ b/src/session.ts @@ -6,7 +6,7 @@ import path = require("path"); import vscode = require("vscode"); import TelemetryReporter, { TelemetryEventProperties, TelemetryEventMeasurements } from "@vscode/extension-telemetry"; import { Message, Trace } from "vscode-jsonrpc"; -import { ILogger, LanguageClientOutputChannelAdapter, LanguageClientTraceFormatter, PsesMergedOutputChannel } from "./logging"; +import { ILogger, LanguageClientTraceFormatter, PsesMergedOutputChannel } from "./logging"; import { PowerShellProcess } from "./process"; import { Settings, changeSetting, getSettings, getEffectiveConfigurationTarget, validateCwdSetting } from "./settings"; import utils = require("./utils"); From f3e73f71bae155bd95733b68051dadc5257de33a Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 14 Nov 2024 18:38:06 -0800 Subject: [PATCH 15/22] Update settings for new EditorServicesLogLevels --- package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 4ffb9a4231..f1d84e4bb5 100644 --- a/package.json +++ b/package.json @@ -916,20 +916,20 @@ }, "powershell.developer.editorServicesLogLevel": { "type": "string", - "default": "Normal", + "default": "Warning", "enum": [ - "Diagnostic", - "Verbose", - "Normal", + "Trace", + "Debug", + "Information", "Warning", "Error", "None" ], "markdownEnumDescriptions": [ "Enables all logging possible, please use this setting when submitting logs for bug reports!", - "Enables more logging than normal.", - "The default logging level.", - "Only log warnings and errors.", + "Enables more detailed logging of the extension", + "Logs high-level information about what the extension is doing.", + "Only log warnings and errors. This is the default setting", "Only log errors.", "Disable all logging possible. No log files will be written!" ], From db3aefb7ab64bfa05986557e1b4e7e9264f2cdba Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 14 Nov 2024 20:04:32 -0800 Subject: [PATCH 16/22] Wire up loglevels in-band due to LSP bug --- src/logging.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/logging.ts b/src/logging.ts index 385c424936..b31fcbbc0d 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -153,8 +153,10 @@ export class LanguageClientOutputChannelAdapter implements LogOutputChannel { this.sendLogMessage(parsedMessage, level); } + // We include the log level inline from PSES for VSCode because our LanguageClient doesn't support middleware for logMessages yet. + // BUG: protected parse(message: string): [string, LogLevel] { - const logLevelMatch = /^\[(?Trace|Debug|Info|Warn|Error) +- \d+:\d+:\d+ [AP]M\] (?.+)/.exec(message); + const logLevelMatch = /^<(?Trace|Debug|Info|Warning|Error)>(?.+)/.exec(message); if (logLevelMatch) { const { level, message } = logLevelMatch.groups!; let logLevel: LogLevel; @@ -168,7 +170,7 @@ export class LanguageClientOutputChannelAdapter implements LogOutputChannel { case "Info": logLevel = LogLevel.Info; break; - case "Warn": + case "Warning": logLevel = LogLevel.Warning; break; case "Error": From cd8aea97106ed147ecc688c828258d451440f9ce Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 14 Nov 2024 22:17:26 -0800 Subject: [PATCH 17/22] Rework multiple classes into a parser function injection --- src/logging.ts | 126 ++++++++++++++++++++----------------------------- src/session.ts | 6 +-- 2 files changed, 55 insertions(+), 77 deletions(-) diff --git a/src/logging.ts b/src/logging.ts index b31fcbbc0d..c24d0a3e3e 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -140,7 +140,16 @@ export class Logger implements ILogger { export class LanguageClientOutputChannelAdapter implements LogOutputChannel { private channel: LogOutputChannel; - constructor(public channelName: string) { + /** + * Creates an instance of the logging class. + * + * @param channelName - The name of the output channel. + * @param parser - A function that parses a log message and returns a tuple containing the parsed message and its log level, or undefined if the log should be filtered. + */ + constructor( + channelName: string, + private parser: (message: string) => [string, LogLevel] | undefined = LanguageClientOutputChannelAdapter.omnisharpLspParser.bind(this) + ) { this.channel = window.createOutputChannel(channelName, {log: true}); } @@ -149,41 +158,19 @@ export class LanguageClientOutputChannelAdapter implements LogOutputChannel { } public append(message: string): void { - const [parsedMessage, level] = this.parse(message); - this.sendLogMessage(parsedMessage, level); + const parseResult = this.parser(message); + if (parseResult !== undefined) {this.sendLogMessage(...parseResult);} } - // We include the log level inline from PSES for VSCode because our LanguageClient doesn't support middleware for logMessages yet. - // BUG: - protected parse(message: string): [string, LogLevel] { - const logLevelMatch = /^<(?Trace|Debug|Info|Warning|Error)>(?.+)/.exec(message); - if (logLevelMatch) { - const { level, message } = logLevelMatch.groups!; - let logLevel: LogLevel; - switch (level) { - case "Trace": - logLevel = LogLevel.Trace; - break; - case "Debug": - logLevel = LogLevel.Debug; - break; - case "Info": - logLevel = LogLevel.Info; - break; - case "Warning": - logLevel = LogLevel.Warning; - break; - case "Error": - logLevel = LogLevel.Error; - break; - default: - logLevel = LogLevel.Info; - break; - } - return [message, logLevel]; - } else { - return [message, LogLevel.Info]; - } + /** Converts from Omnisharp logs since middleware for LogMessage does not currently exist **/ + public static omnisharpLspParser(message: string): [string, LogLevel] { + const logLevelMatch = /^\[(?Trace|Debug|Info|Warn|Error) +- \d+:\d+:\d+ [AP]M\] (?.+)/.exec(message); + const logLevel: LogLevel = logLevelMatch?.groups?.level + ? LogLevel[logLevelMatch.groups.level as keyof typeof LogLevel] + : LogLevel.Info; + const logMessage = logLevelMatch?.groups?.message ?? message; + + return [logMessage, logLevel]; } protected sendLogMessage(message: string, level: LogLevel): void { @@ -257,49 +244,40 @@ export class LanguageClientOutputChannelAdapter implements LogOutputChannel { // #endregion } -/** Appends additional */ -export class PsesMergedOutputChannel extends LanguageClientOutputChannelAdapter { - public override appendLine(message: string): void { - this.append(message); - } - - public override append(message: string): void { - const [parsedMessage, level] = this.parse(message); - - // Append PSES prefix to log messages to differentiate them from Client messages - this.sendLogMessage("[PSES] " + parsedMessage, level); - } +/** Special parsing for PowerShell Editor Services LSP messages since the LogLevel cannot be read due to vscode + * LanguageClient Limitations (https://github.com/microsoft/vscode-languageserver-node/issues/1116) + */ +export function PsesParser(message: string): [string, LogLevel] { + const logLevelMatch = /^<(?Trace|Debug|Info|Warning|Error)>(?.+)/.exec(message); + const logLevel: LogLevel = logLevelMatch?.groups?.level + ? LogLevel[logLevelMatch.groups.level as keyof typeof LogLevel] + : LogLevel.Info; + const logMessage = logLevelMatch?.groups?.message ?? message; + + return ["[PSES] " + logMessage, logLevel]; } -/** Overrides the severity of some LSP traces to be more logical */ -export class LanguageClientTraceFormatter extends LanguageClientOutputChannelAdapter { - public override appendLine(message: string): void { - this.append(message); +/** Lsp Trace Parser that does some additional parsing and formatting to make it look nicer */ +export function LspTraceParser(message: string): [string, LogLevel] { + let [parsedMessage, level] = LanguageClientOutputChannelAdapter.omnisharpLspParser(message); + if (parsedMessage.startsWith("Sending ")) { + parsedMessage = parsedMessage.replace("Sending", "➡️"); + level = LogLevel.Debug; + } + if (parsedMessage.startsWith("Received ")) { + parsedMessage = parsedMessage.replace("Received", "⬅️"); + level = LogLevel.Debug; + } + if (parsedMessage.startsWith("Params:") + || parsedMessage.startsWith("Result:") + ) { + level = LogLevel.Trace; } - public override append(message: string): void { - // eslint-disable-next-line prefer-const - let [parsedMessage, level] = this.parse(message); - - if (parsedMessage.startsWith("Sending ")) { - parsedMessage = parsedMessage.replace("Sending", "▶️"); - level = LogLevel.Debug; - } - if (parsedMessage.startsWith("Received ")) { - parsedMessage = parsedMessage.replace("Received", "◀️"); - level = LogLevel.Debug; - } - if (parsedMessage.startsWith("Params:") - || parsedMessage.startsWith("Result:") - ) { - level = LogLevel.Trace; - } - - // These are PSES messages we don't really need to see so we drop these to trace - if (parsedMessage.startsWith("◀️ notification 'window/logMessage'")) { - level = LogLevel.Trace; - } - - this.sendLogMessage(parsedMessage.trimEnd(), level); + // These are PSES messages we don't really need to see so we drop these to trace + if (parsedMessage.startsWith("⬅️ notification 'window/logMessage'")) { + level = LogLevel.Trace; } + + return [parsedMessage.trimEnd(), level]; } diff --git a/src/session.ts b/src/session.ts index c7b8afa043..524a394131 100644 --- a/src/session.ts +++ b/src/session.ts @@ -6,7 +6,7 @@ import path = require("path"); import vscode = require("vscode"); import TelemetryReporter, { TelemetryEventProperties, TelemetryEventMeasurements } from "@vscode/extension-telemetry"; import { Message, Trace } from "vscode-jsonrpc"; -import { ILogger, LanguageClientTraceFormatter, PsesMergedOutputChannel } from "./logging"; +import { ILogger, LanguageClientOutputChannelAdapter, LspTraceParser, PsesParser } from "./logging"; import { PowerShellProcess } from "./process"; import { Settings, changeSetting, getSettings, getEffectiveConfigurationTarget, validateCwdSetting } from "./settings"; import utils = require("./utils"); @@ -688,9 +688,9 @@ export class SessionManager implements Middleware { }, }, middleware: this, - traceOutputChannel: new LanguageClientTraceFormatter("PowerShell: Trace LSP"), + traceOutputChannel: new LanguageClientOutputChannelAdapter("PowerShell: Trace LSP", LspTraceParser), // This is named the same as the Client log to merge the logs, but will be handled and disposed separately. - outputChannel: new PsesMergedOutputChannel("PowerShell"), + outputChannel: new LanguageClientOutputChannelAdapter("PowerShell", PsesParser), revealOutputChannelOn: RevealOutputChannelOn.Never }; From 1bf5e4150d200cd64768118e036794c56d6a2788 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 14 Nov 2024 22:40:05 -0800 Subject: [PATCH 18/22] Fix some glyphs --- src/features/DebugSession.ts | 1 + src/logging.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/features/DebugSession.ts b/src/features/DebugSession.ts index 84bd573f42..39df55d1c5 100644 --- a/src/features/DebugSession.ts +++ b/src/features/DebugSession.ts @@ -626,6 +626,7 @@ class PowerShellDebugAdapterTrackerFactory implements DebugAdapterTrackerFactory return this._log; } + // This tracker effectively implements the logging for the debug adapter to a LogOutputChannel createDebugAdapterTracker(session: DebugSession): DebugAdapterTracker { const sessionInfo = `${this.adapterName} Debug Session: ${session.name} [${session.id}]`; return { diff --git a/src/logging.ts b/src/logging.ts index c24d0a3e3e..8e31f18e7f 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -274,7 +274,7 @@ export function LspTraceParser(message: string): [string, LogLevel] { level = LogLevel.Trace; } - // These are PSES messages we don't really need to see so we drop these to trace + // These are PSES messages that get logged to the output channel anyways so we drop these to trace for easy noise filtering if (parsedMessage.startsWith("⬅️ notification 'window/logMessage'")) { level = LogLevel.Trace; } From 1c521c594b22e0725c045bac2c8577c341b838bf Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 14 Nov 2024 22:47:24 -0800 Subject: [PATCH 19/22] Revert extra config settings for dynamic log configuration for now --- package.json | 8 +------- src/logging.ts | 14 ++++++++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index f1d84e4bb5..da3294d9cf 100644 --- a/package.json +++ b/package.json @@ -953,11 +953,6 @@ "default": [], "markdownDescription": "An array of strings that enable experimental features in the PowerShell extension. **No flags are currently available!**" }, - "powershell.developer.traceLsp": { - "type": "boolean", - "default": false, - "markdownDescription": "Traces the LSP communication between VS Code and the PowerShell Editor Services [LSP Server](https://microsoft.github.io/language-server-protocol/). The output will be logged and also visible in the Output pane, where the verbosity is configurable. **For extension developers and issue troubleshooting only!**" - }, "powershell.developer.traceDap": { "type": "boolean", "default": false, @@ -971,8 +966,7 @@ "verbose" ], "default": "off", - "markdownDescription": "Traces the communication between VS Code and the PowerShell Editor Services [LSP Server](https://microsoft.github.io/language-server-protocol/). The output will be logged and also visible in the Output pane, where the verbosity is configurable. **For extension developers and issue troubleshooting only!**", - "markdownDeprecationMessage": "Please use the `powershell.developer.traceLsp` setting instead and control verbosity via the Output Window settings." + "markdownDescription": "Traces the communication between VS Code and the PowerShell Editor Services [LSP Server](https://microsoft.github.io/language-server-protocol/). The output will be logged and also visible in the Output pane, where the verbosity is configurable. **For extension developers and issue troubleshooting only!**" }, "powershell.developer.waitForSessionFileTimeoutSeconds": { "type": "number", diff --git a/src/logging.ts b/src/logging.ts index 8e31f18e7f..a7d001fdd9 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -138,7 +138,13 @@ export class Logger implements ILogger { * HACK: This is for legacy compatability and can be removed when https://github.com/microsoft/vscode-languageserver-node/issues/1116 is merged and replaced with a normal LogOutputChannel. We don't use a middleware here because any direct logging calls like client.warn() and server-initiated messages would not be captured by middleware. */ export class LanguageClientOutputChannelAdapter implements LogOutputChannel { - private channel: LogOutputChannel; + private _channel: LogOutputChannel | undefined; + private get channel(): LogOutputChannel { + if (!this._channel) { + this._channel = window.createOutputChannel(this.channelName, {log: true}); + } + return this._channel; + } /** * Creates an instance of the logging class. @@ -147,10 +153,9 @@ export class LanguageClientOutputChannelAdapter implements LogOutputChannel { * @param parser - A function that parses a log message and returns a tuple containing the parsed message and its log level, or undefined if the log should be filtered. */ constructor( - channelName: string, + private channelName: string, private parser: (message: string) => [string, LogLevel] | undefined = LanguageClientOutputChannelAdapter.omnisharpLspParser.bind(this) ) { - this.channel = window.createOutputChannel(channelName, {log: true}); } public appendLine(message: string): void { @@ -198,7 +203,8 @@ export class LanguageClientOutputChannelAdapter implements LogOutputChannel { // #region Passthru Implementation public get name(): string { - return this.channel.name; + // prevents the window from being created unless we get a log request + return this.channelName; } public get logLevel(): LogLevel { return this.channel.logLevel; From 5b30d0f7b5c0f7467faa218817a745eb360bddec Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 14 Nov 2024 22:50:17 -0800 Subject: [PATCH 20/22] Remove SetLSPTrace for now --- src/session.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/session.ts b/src/session.ts index 524a394131..c4baeb15d1 100644 --- a/src/session.ts +++ b/src/session.ts @@ -744,19 +744,6 @@ export class SessionManager implements Middleware { return languageClient; } - /** Synchronizes a vscode LogOutputChannel log level to the LSP trace setting to minimize traffic */ - private async setLspTrace(languageClient: LanguageClient, level: vscode.LogLevel): Promise { - this.logger.writeVerbose("LSP Trace level changed to: " + level.toString()); - if (level == vscode.LogLevel.Trace) { - return languageClient.setTrace(Trace.Verbose); - } else if (level == vscode.LogLevel.Debug) { - return languageClient.setTrace(Trace.Messages); - } else { - return languageClient.setTrace(Trace.Off); - } - } - - private async getBundledModulesPath(): Promise { // Because the extension is always at `/out/main.js` let bundledModulesPath = path.resolve(__dirname, "../modules"); From 60ddcce940b36219f14b00e7040b6f8ee5f0a41c Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 14 Nov 2024 22:52:18 -0800 Subject: [PATCH 21/22] Clean import --- src/session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/session.ts b/src/session.ts index c4baeb15d1..40a125aa7f 100644 --- a/src/session.ts +++ b/src/session.ts @@ -5,7 +5,7 @@ import net = require("net"); import path = require("path"); import vscode = require("vscode"); import TelemetryReporter, { TelemetryEventProperties, TelemetryEventMeasurements } from "@vscode/extension-telemetry"; -import { Message, Trace } from "vscode-jsonrpc"; +import { Message } from "vscode-jsonrpc"; import { ILogger, LanguageClientOutputChannelAdapter, LspTraceParser, PsesParser } from "./logging"; import { PowerShellProcess } from "./process"; import { Settings, changeSetting, getSettings, getEffectiveConfigurationTarget, validateCwdSetting } from "./settings"; From 79954e20a01e5d172d89e04b3845aa0519f72ed2 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 14 Nov 2024 23:18:03 -0800 Subject: [PATCH 22/22] Align logging terminology to vscode output windows and remove editorServices from options definitions --- docs/troubleshooting.md | 4 ++-- package.json | 2 +- src/extension.ts | 2 +- src/features/DebugSession.ts | 12 ++++++------ src/features/ExternalApi.ts | 8 ++++---- src/features/UpdatePowerShell.ts | 24 ++++++++++++------------ src/logging.ts | 8 ++++---- src/process.ts | 10 +++++----- src/session.ts | 28 ++++++++++++++-------------- src/settings.ts | 16 +++------------- test/utils.ts | 4 ++-- 11 files changed, 54 insertions(+), 64 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 3c341a6e19..c6ccb7dbdc 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -253,11 +253,11 @@ Logs provide context for what was happening when the issue occurred. **You shoul your logs for any sensitive information you would not like to share online!** * Before sending through logs, try and reproduce the issue with **log level set to - Diagnostic**. You can set this in the [VS Code Settings][] + Trace**. You can set this in the [VS Code Settings][] (Ctrl+,) with: ```json - "powershell.developer.editorServicesLogLevel": "Diagnostic" + "powershell.developer.editorServicesLogLevel": "Trace" ``` * After you have captured the issue with the log level turned up, you may want to return diff --git a/package.json b/package.json index da3294d9cf..73687b5fae 100644 --- a/package.json +++ b/package.json @@ -933,7 +933,7 @@ "Only log errors.", "Disable all logging possible. No log files will be written!" ], - "markdownDescription": "Sets the log verbosity for both the extension and its LSP server, PowerShell Editor Services. **Please set to `Diagnostic` when recording logs for a bug report!**" + "markdownDescription": "Sets the log verbosity for both the extension and its LSP server, PowerShell Editor Services. **Please set to `Trace` when recording logs for a bug report!**" }, "powershell.developer.editorServicesWaitForDebugger": { "type": "boolean", diff --git a/src/extension.ts b/src/extension.ts index df943bd32b..8676724ab1 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -48,7 +48,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { const extension = this.getRegisteredExtension(uuid); - this.logger.writeVerbose(`Extension '${extension.id}' called 'getPowerShellVersionDetails'.`); + this.logger.writeDebug(`Extension '${extension.id}' called 'getPowerShellVersionDetails'.`); await this.sessionManager.waitUntilStarted(); const versionDetails = this.sessionManager.getPowerShellVersionDetails(); @@ -162,7 +162,7 @@ export class ExternalApiFeature implements IPowerShellExtensionClient { */ public async waitUntilStarted(uuid = ""): Promise { const extension = this.getRegisteredExtension(uuid); - this.logger.writeVerbose(`Extension '${extension.id}' called 'waitUntilStarted'.`); + this.logger.writeDebug(`Extension '${extension.id}' called 'waitUntilStarted'.`); await this.sessionManager.waitUntilStarted(); } diff --git a/src/features/UpdatePowerShell.ts b/src/features/UpdatePowerShell.ts index 01c31eb385..6805272cc8 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.writeVerbose("Setting 'promptToUpdatePowerShell' was false."); + this.logger.writeDebug("Setting 'promptToUpdatePowerShell' was false."); return false; } // Respect environment configuration. if (process.env.POWERSHELL_UPDATECHECK?.toLowerCase() === "off") { - this.logger.writeVerbose("Environment variable 'POWERSHELL_UPDATECHECK' was 'Off'."); + this.logger.writeDebug("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.writeVerbose("Not prompting to update Windows PowerShell."); + this.logger.writeDebug("Not prompting to update Windows PowerShell."); return false; } @@ -78,13 +78,13 @@ export class UpdatePowerShell { // Skip if PowerShell is self-built, that is, this contains a commit hash. if (commit.length >= 40) { - this.logger.writeVerbose("Not prompting to update development build."); + this.logger.writeDebug("Not prompting to update development build."); return false; } // Skip if preview is a daily build. if (daily.toLowerCase().startsWith("daily")) { - this.logger.writeVerbose("Not prompting to update daily build."); + this.logger.writeDebug("Not prompting to update daily build."); return false; } } @@ -106,7 +106,7 @@ export class UpdatePowerShell { // "ReleaseTag": "v7.2.7" // } const data = await response.json(); - this.logger.writeVerbose(`Received from '${url}':\n${JSON.stringify(data, undefined, 2)}`); + this.logger.writeDebug(`Received from '${url}':\n${JSON.stringify(data, undefined, 2)}`); return data.ReleaseTag; } @@ -115,18 +115,18 @@ export class UpdatePowerShell { return undefined; } - this.logger.writeVerbose("Checking for PowerShell update..."); + this.logger.writeDebug("Checking for PowerShell update..."); const tags: string[] = []; if (process.env.POWERSHELL_UPDATECHECK?.toLowerCase() === "lts") { // Only check for update to LTS. - this.logger.writeVerbose("Checking for LTS update..."); + this.logger.writeDebug("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.writeVerbose("Checking for stable update..."); + this.logger.writeDebug("Checking for stable update..."); const tag = await this.getRemoteVersion(UpdatePowerShell.StableBuildInfoURL); if (tag != undefined) { tags.push(tag); @@ -134,7 +134,7 @@ export class UpdatePowerShell { // Also check for a preview update. if (this.localVersion.prerelease.length > 0) { - this.logger.writeVerbose("Checking for preview update..."); + this.logger.writeDebug("Checking for preview update..."); const tag = await this.getRemoteVersion(UpdatePowerShell.PreviewBuildInfoURL); if (tag != undefined) { tags.push(tag); @@ -181,11 +181,11 @@ export class UpdatePowerShell { // If the user cancels the notification. if (!result) { - this.logger.writeVerbose("User canceled PowerShell update prompt."); + this.logger.writeDebug("User canceled PowerShell update prompt."); return; } - this.logger.writeVerbose(`User said '${UpdatePowerShell.promptOptions[result.id].title}'.`); + this.logger.writeDebug(`User said '${UpdatePowerShell.promptOptions[result.id].title}'.`); switch (result.id) { // Yes diff --git a/src/logging.ts b/src/logging.ts index a7d001fdd9..7ce8d09e16 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -9,8 +9,8 @@ import { LogOutputChannel, LogLevel, window, Event } from "vscode"; export interface ILogger { write(message: string, ...additionalMessages: string[]): void; writeAndShowInformation(message: string, ...additionalMessages: string[]): Promise; - writeDiagnostic(message: string, ...additionalMessages: string[]): void; - writeVerbose(message: string, ...additionalMessages: string[]): void; + writeTrace(message: string, ...additionalMessages: string[]): void; + writeDebug(message: string, ...additionalMessages: string[]): void; writeWarning(message: string, ...additionalMessages: string[]): void; writeAndShowWarning(message: string, ...additionalMessages: string[]): Promise; writeError(message: string, ...additionalMessages: string[]): void; @@ -56,11 +56,11 @@ export class Logger implements ILogger { } } - public writeDiagnostic(message: string, ...additionalMessages: string[]): void { + public writeTrace(message: string, ...additionalMessages: string[]): void { this.writeAtLevel(LogLevel.Trace, message, ...additionalMessages); } - public writeVerbose(message: string, ...additionalMessages: string[]): void { + public writeDebug(message: string, ...additionalMessages: string[]): void { this.writeAtLevel(LogLevel.Debug, message, ...additionalMessages); } diff --git a/src/process.ts b/src/process.ts index 5e0d696321..052da2b8bf 100644 --- a/src/process.ts +++ b/src/process.ts @@ -90,13 +90,13 @@ export class PowerShellProcess { startEditorServices); } else { // Otherwise use -EncodedCommand for better quote support. - this.logger.writeVerbose("Using Base64 -EncodedCommand but logging as -Command equivalent."); + this.logger.writeDebug("Using Base64 -EncodedCommand but logging as -Command equivalent."); powerShellArgs.push( "-EncodedCommand", Buffer.from(startEditorServices, "utf16le").toString("base64")); } - this.logger.writeVerbose(`Starting process: ${this.exePath} ${powerShellArgs.slice(0, -2).join(" ")} -Command ${startEditorServices}`); + this.logger.writeDebug(`Starting process: ${this.exePath} ${powerShellArgs.slice(0, -2).join(" ")} -Command ${startEditorServices}`); // Make sure no old session file exists await this.deleteSessionFile(this.sessionFilePath); @@ -174,7 +174,7 @@ export class PowerShellProcess { } public dispose(): void { - this.logger.writeVerbose(`Disposing PowerShell process with PID: ${this.pid}`); + this.logger.writeDebug(`Disposing PowerShell process with PID: ${this.pid}`); void this.deleteSessionFile(this.sessionFilePath); @@ -227,7 +227,7 @@ export class PowerShellProcess { const warnAt = numOfTries - PowerShellProcess.warnUserThreshold; // Check every second. - this.logger.writeVerbose(`Waiting for session file: ${this.sessionFilePath}`); + this.logger.writeDebug(`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."); @@ -240,7 +240,7 @@ export class PowerShellProcess { } if (await utils.checkIfFileExists(this.sessionFilePath)) { - this.logger.writeVerbose("Session file found."); + this.logger.writeDebug("Session file found."); return await this.readSessionFile(this.sessionFilePath); } diff --git a/src/session.ts b/src/session.ts index 40a125aa7f..0b1037a116 100644 --- a/src/session.ts +++ b/src/session.ts @@ -162,7 +162,7 @@ export class SessionManager implements Middleware { return; case SessionStatus.Running: // We're started, just return. - this.logger.writeVerbose("Already started."); + this.logger.writeDebug("Already started."); return; case SessionStatus.Busy: // We're started but busy so notify and return. @@ -171,12 +171,12 @@ export class SessionManager implements Middleware { return; case SessionStatus.Stopping: // Wait until done stopping, then start. - this.logger.writeVerbose("Still stopping."); + this.logger.writeDebug("Still stopping."); await this.waitWhileStopping(); break; case SessionStatus.Failed: // Try to start again. - this.logger.writeVerbose("Previously failed, starting again."); + this.logger.writeDebug("Previously failed, starting again."); break; } @@ -294,7 +294,7 @@ export class SessionManager implements Middleware { if (exeNameOverride) { // Reset the version and PowerShell details since we're launching a // new executable. - this.logger.writeVerbose(`Starting with executable overriden to: ${exeNameOverride}`); + this.logger.writeDebug(`Starting with executable overriden to: ${exeNameOverride}`); this.sessionSettings.powerShellDefaultVersion = exeNameOverride; this.versionDetails = undefined; this.PowerShellExeDetails = undefined; @@ -475,7 +475,8 @@ export class SessionManager implements Middleware { // Detect any setting changes that would affect the session. const coldRestartSettingNames = [ "developer.traceLsp", - "developer.traceDap" + "developer.traceDap", + "developer.editorServicesLogLevel", ]; for (const settingName of coldRestartSettingNames) { if (changeEvent.affectsConfiguration("powershell" + "." + settingName)) { @@ -486,7 +487,6 @@ export class SessionManager implements Middleware { // TODO: Migrate these to affectsConfiguration style above if (settings.cwd !== this.sessionSettings.cwd || settings.powerShellDefaultVersion !== this.sessionSettings.powerShellDefaultVersion - || settings.developer.editorServicesLogLevel !== this.sessionSettings.developer.editorServicesLogLevel || settings.developer.bundledModulesPath !== this.sessionSettings.developer.bundledModulesPath || settings.developer.editorServicesWaitForDebugger !== this.sessionSettings.developer.editorServicesWaitForDebugger || settings.developer.setExecutionPolicy !== this.sessionSettings.developer.setExecutionPolicy @@ -499,7 +499,7 @@ export class SessionManager implements Middleware { } private async restartWithPrompt(): Promise { - this.logger.writeVerbose("Settings changed, prompting to restart..."); + this.logger.writeDebug("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"); @@ -520,7 +520,7 @@ export class SessionManager implements Middleware { } private async findPowerShell(): Promise { - this.logger.writeVerbose("Finding PowerShell..."); + this.logger.writeDebug("Finding PowerShell..."); const powershellExeFinder = new PowerShellExeFinder( this.platformDetails, this.sessionSettings.powerShellAdditionalExePaths, @@ -619,7 +619,7 @@ export class SessionManager implements Middleware { } private sessionStarted(sessionDetails: IEditorServicesSessionDetails): boolean { - this.logger.writeVerbose(`Session details: ${JSON.stringify(sessionDetails, undefined, 2)}`); + this.logger.writeDebug(`Session details: ${JSON.stringify(sessionDetails, undefined, 2)}`); if (sessionDetails.status === "started") { // Successful server start with a session file return true; } @@ -638,7 +638,7 @@ export class SessionManager implements Middleware { } private async startLanguageClient(sessionDetails: IEditorServicesSessionDetails): Promise { - this.logger.writeVerbose("Connecting to language service..."); + this.logger.writeDebug("Connecting to language service..."); const connectFunc = (): Promise => { return new Promise( (resolve, _reject) => { @@ -646,7 +646,7 @@ export class SessionManager implements Middleware { socket.on( "connect", () => { - this.logger.writeVerbose("Language service connected."); + this.logger.writeDebug("Language service connected."); resolve({ writer: socket, reader: socket }); }); }); @@ -796,8 +796,8 @@ Type 'help' to get help. && this.extensionContext.extensionMode === vscode.ExtensionMode.Development) { editorServicesArgs += "-WaitForDebugger "; } - - editorServicesArgs += `-LogLevel '${this.sessionSettings.developer.editorServicesLogLevel}' `; + const logLevel = vscode.workspace.getConfiguration("powershell.developer").get("editorServicesLogLevel"); + editorServicesArgs += `-LogLevel '${logLevel}' `; return editorServicesArgs; } @@ -869,7 +869,7 @@ Type 'help' to get help. } private setSessionStatus(detail: string, status: SessionStatus): void { - this.logger.writeVerbose(`Session status changing from '${this.sessionStatus}' to '${status}'.`); + this.logger.writeDebug(`Session status changing from '${this.sessionStatus}' to '${status}'.`); this.sessionStatus = status; this.languageStatusItem.text = "$(terminal-powershell)"; this.languageStatusItem.detail = "PowerShell"; diff --git a/src/settings.ts b/src/settings.ts index f378b4ab68..f29079846a 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -56,15 +56,6 @@ export enum PipelineIndentationStyle { None = "None", } -export enum LogLevel { - Diagnostic = "Diagnostic", - Verbose = "Verbose", - Normal = "Normal", - Warning = "Warning", - Error = "Error", - None = "None", -} - export enum CommentType { Disabled = "Disabled", BlockComment = "BlockComment", @@ -120,7 +111,6 @@ class DeveloperSettings extends PartialSettings { // From `/out/main.js` we go to the directory before and // then into the other repo. bundledModulesPath = "../../PowerShellEditorServices/module"; - editorServicesLogLevel = LogLevel.Normal; editorServicesWaitForDebugger = false; setExecutionPolicy = true; waitForSessionFileTimeoutSeconds = 240; @@ -209,7 +199,7 @@ export async function changeSetting( configurationTarget: vscode.ConfigurationTarget | boolean | undefined, logger: ILogger | undefined): Promise { - logger?.writeVerbose(`Changing '${settingName}' at scope '${configurationTarget}' to '${newValue}'.`); + logger?.writeDebug(`Changing '${settingName}' at scope '${configurationTarget}' to '${newValue}'.`); try { const configuration = vscode.workspace.getConfiguration(utils.PowerShellLanguageId); @@ -242,7 +232,7 @@ export async function getChosenWorkspace(logger: ILogger | undefined): Promise { return Promise.resolve(); } - writeDiagnostic(_message: string, ..._additionalMessages: string[]): void { + writeTrace(_message: string, ..._additionalMessages: string[]): void { return; } - writeVerbose(_message: string, ..._additionalMessages: string[]): void { + writeDebug(_message: string, ..._additionalMessages: string[]): void { return; } writeWarning(_message: string, ..._additionalMessages: string[]): void {