Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Implement LogOutputWindow for Logging #5065

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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!**"
}
}
},
Expand Down
6 changes: 2 additions & 4 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -47,9 +47,7 @@ const documentSelector: DocumentSelector = [
];

export async function activate(context: vscode.ExtensionContext): Promise<IPowerShellExtensionClient> {
const logLevel = vscode.workspace.getConfiguration(`${PowerShellLanguageId}.developer`)
.get<string>("editorServicesLogLevel", LogLevel.Normal);
logger = new Logger(logLevel, context.globalStorageUri);
logger = new Logger(context.logUri);

telemetryReporter = new TelemetryReporter(TELEMETRY_KEY);

Expand Down
32 changes: 18 additions & 14 deletions src/features/DebugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>("powerShell/startDebugger");
Expand Down Expand Up @@ -608,17 +608,9 @@ class PowerShellDebugAdapterTrackerFactory implements DebugAdapterTrackerFactory
disposables: Disposable[] = [];
dapLogEnabled: boolean = workspace.getConfiguration("powershell").get<boolean>("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<boolean>("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
Expand All @@ -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 {
Expand Down Expand Up @@ -663,6 +665,8 @@ class PowerShellDebugAdapterTrackerFactory implements DebugAdapterTrackerFactory
};
}



dispose(): void {
this.disposables.forEach(d => d.dispose());
}
Expand Down
139 changes: 37 additions & 102 deletions src/logging.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,13 @@
// 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 { 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.
*/
export interface ILogger {
logDirectoryPath: vscode.Uri;
updateLogLevel(logLevelName: string): void;
logDirectoryPath: Uri;
write(message: string, ...additionalMessages: string[]): void;
writeAndShowInformation(message: string, ...additionalMessages: string[]): Promise<void>;
writeDiagnostic(message: string, ...additionalMessages: string[]): void;
Expand All @@ -35,37 +22,29 @@ 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
private logDirectoryCreated = false;
private writingLog = false;

constructor(logLevelName: string, globalStorageUri: vscode.Uri) {
this.logLevel = Logger.logLevelNameToValue(logLevelName);
this.logChannel = vscode.window.createOutputChannel("PowerShell Extension 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;
public get logLevel(): LogLevel { return this.logChannel.logLevel;}

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 = vscode.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");
this.logDirectoryPath = logPath;

// 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.Off) {
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(); }),
];
Expand All @@ -89,24 +68,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<void> {
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 {
Expand All @@ -116,7 +95,7 @@ export class Logger implements ILogger {
public async writeAndShowWarning(message: string, ...additionalMessages: string[]): Promise<void> {
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();
}
Expand All @@ -129,7 +108,7 @@ export class Logger implements ILogger {
public async writeAndShowError(message: string, ...additionalMessages: string[]): Promise<void> {
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();
}
Expand All @@ -147,7 +126,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 ) {
Expand All @@ -158,70 +137,26 @@ 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();
}

private async openLogFolder(): Promise<void> {
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);
} 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}`;
}

// TODO: Should we await this function above?
private async writeLine(message: string, level: LogLevel = LogLevel.Normal): Promise<void> {
const timestampedMessage = Logger.timestampMessage(message, level);
this.logChannel.appendLine(timestampedMessage);
if (this.logLevel !== LogLevel.None) {
// A simple lock because this function isn't re-entrant.
while (this.writingLog) {
await utils.sleep(300);
await commands.executeCommand("openFolder", this.logDirectoryPath, true);
}

private async writeLine(message: string, level: LogLevel = LogLevel.Info): Promise<void> {
return new Promise<void>((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 vscode.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);
}
await vscode.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();
});
}
}
Loading
Loading