You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
vscode-sshfs/src/logging.ts

192 lines
8.2 KiB

import * as vscode from 'vscode';
import type { FileSystemConfig } from './fileSystemConfig';
// Since the Extension Development Host runs with debugger, we can use this to detect if we're debugging.
// The only things it currently does is copying Logging messages to the console, while also enabling
// the webview (Settings UI) from trying a local dev server first instead of the pre-built version.
export let DEBUG: boolean = false;
export function setDebug(debug: boolean) {
console.warn(`[vscode-sshfs] Debug mode set to ${debug}`);
DEBUG = debug;
if (!debug) return;
try { require('../.pnp.cjs').setup(); } catch (e) {
console.warn('Could not set up .pnp.cjs:', e);
}
try { require('source-map-support').install(); } catch (e) {
console.warn('Could not install source-map-support:', e);
}
}
const outputChannel = vscode.window.createOutputChannel('SSH FS');
export interface LoggingOptions {
/** The level of outputting the logger's name/stacktrace:
* 0: Don't report anything
* 1: Only report the name (or first line of stacktrace if missing)
* 2: Report name and stacktrace (if available)
*/
reportedFromLevel: number;
/** Whether to output a stacktrace of the .info() call etc
* 0: Don't output a stacktrace
* -1: Output the whole stacktrace
* N: Only output the first N frames
*/
callStacktrace: number;
/** Used with .callStacktrace to skip the given amount of stacktraces in the beginning.
* Useful when .info() etc is called from a helper function which itself isn't worth logging the stacktrace of.
* Defaults to 0 meaning no offset.
*/
callStacktraceOffset: number;
4 years ago
/** Used when the "message" to be logged is an Error object with a stack.
* 0: Don't output the stack
* -1: Output the whole stack
* N: Only output the first N lines
* The stack gets stringified in the first logger, so child loggers don't inherit, it uses the default (which is 0)
*/
maxErrorStack: number;
}
export const LOGGING_NO_STACKTRACE: Partial<LoggingOptions> = { callStacktrace: 0 };
export const LOGGING_SINGLE_LINE_STACKTRACE: Partial<LoggingOptions> = { callStacktrace: 1 };
function hasPromiseCause(error: Error): error is Error & { promiseCause: string } {
return typeof (error as any).promiseCause === 'string';
}
4 years ago
export type LoggerDefaultLevels = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR';
class Logger {
protected parent?: Logger;
protected stack?: string;
protected defaultLoggingOptions: LoggingOptions = {
reportedFromLevel: 0,
callStacktrace: 0,
callStacktraceOffset: 0,
4 years ago
maxErrorStack: 0,
};
public overriddenTypeOptions: { [type in LoggerDefaultLevels]?: Partial<LoggingOptions> } = {
4 years ago
WARNING: { callStacktrace: 3, reportedFromLevel: 2 },
ERROR: { callStacktrace: 5, reportedFromLevel: 2, maxErrorStack: 10 },
};
protected constructor(protected name?: string, generateStack: number | boolean = false) {
if (generateStack) {
const len = typeof generateStack === 'number' ? generateStack : 5;
let stack = new Error().stack;
stack = stack && stack.split('\n').slice(3, 3 + len).join('\n');
this.stack = stack || '<stack unavailable>';
}
}
protected do_print(type: string, message: string, options: LoggingOptions) {
options = { ...this.defaultLoggingOptions, ...options };
const { reportedFromLevel } = options;
// Calculate prefix
const prefix = this.name ? `[${this.name}] ` : '';
// Calculate suffix
let suffix = '';
if (this.name && this.stack && reportedFromLevel >= 2) {
suffix = `\nReported from ${this.name}:\n${this.stack}`;
} else if (this.name && reportedFromLevel >= 1) {
suffix = `\nReported from ${this.name}`;
} else if (this.stack && reportedFromLevel >= 2) {
suffix = `\nReported from:\n${this.stack}`;
}
// If there is a parent logger, pass the message with prefix/suffix on
if (this.parent) return this.parent.do_print(type, `${prefix}${message}${suffix}`, options);
// There is no parent, we're responsible for actually logging the message
const space = ' '.repeat(Math.max(0, 8 - type.length));
const msg = `[${type}]${space}${prefix}${message}${suffix}`
outputChannel.appendLine(msg);
if (DEBUG) (console[type.toLowerCase()] || console.log).call(console, msg);
}
protected print(type: string, message: string | Error, partialOptions?: Partial<LoggingOptions>) {
4 years ago
type = type.toUpperCase();
const options: LoggingOptions = { ...this.defaultLoggingOptions, ...this.overriddenTypeOptions[type], ...partialOptions };
4 years ago
// Format errors with stacktraces to display the JSON and the stacktrace if needed
if (message instanceof Error && message.stack) {
4 years ago
let msg = message.message;
try {
const json = JSON.stringify(message);
if (json !== '{}') msg += `\nJSON: ${json}`;
4 years ago
} finally { }
const { maxErrorStack } = options;
if (message.stack && maxErrorStack) {
let { stack } = message;
if (maxErrorStack > 0) {
stack = stack.split(/\n/g).slice(0, maxErrorStack + 1).join('\n');
4 years ago
}
msg += '\n' + stack;
}
if (hasPromiseCause(message) && maxErrorStack) {
let { promiseCause } = message;
if (maxErrorStack > 0) {
promiseCause = promiseCause.split(/\n/g).slice(1, maxErrorStack + 1).join('\n');
}
msg += '\nCaused by promise:\n' + promiseCause;
}
4 years ago
message = msg;
}
// Do we need to also output a stacktrace?
const { callStacktrace, callStacktraceOffset = 0 } = options;
if (callStacktrace) {
let stack = new Error().stack;
let split = stack && stack.split('\n');
split = split && split.slice(callStacktraceOffset + 3, callStacktrace > 0 ? callStacktraceOffset + 3 + callStacktrace : undefined);
stack = split ? split.join('\n') : '<stack unavailable>';
message += `\nLogged at:\n${stack}`;
}
// Start the (recursive parent-related) printing
this.do_print(type, `${message}`, options as LoggingOptions)
}
public scope(name?: string, generateStack: number | boolean = false) {
const logger = new Logger(name, generateStack);
logger.parent = this;
return logger;
}
public debug(message: string, options: Partial<LoggingOptions> = {}) {
this.print('DEBUG', message, options);
}
public info(message: string, options: Partial<LoggingOptions> = {}) {
this.print('INFO', message, options);
}
public warning(message: string | Error, options: Partial<LoggingOptions> = {}) {
this.print('WARNING', message, options);
}
public error(message: string | Error, options: Partial<LoggingOptions> = {}) {
this.print('ERROR', message, options);
6 years ago
}
}
export type { Logger };
export function withStacktraceOffset(amount: number = 1, options: Partial<LoggingOptions> = {}): Partial<LoggingOptions> {
return { ...options, callStacktraceOffset: (options.callStacktraceOffset || 0) + amount };
}
export interface CensoredFileSystemConfig extends Omit<FileSystemConfig, 'sock' | '_calculated'> {
sock?: string;
_calculated?: CensoredFileSystemConfig;
}
export function censorConfig(config: FileSystemConfig): CensoredFileSystemConfig {
return {
...config,
password: typeof config.password === 'string' ? '<censored>' : config.password,
passphrase: typeof config.passphrase === 'string' ? '<censored>' : config.passphrase,
6 years ago
privateKey: config.privateKey instanceof Buffer ? `Buffer(${config.privateKey.length})` : config.privateKey,
sock: config.sock ? '<socket>' : config.sock,
_calculated: config._calculated ? censorConfig(config._calculated) : config._calculated,
};
}
export const Logging = new (Logger as any) as Logger;
Logging.info('Created output channel for vscode-sshfs');
Logging.info(`When posting your logs somewhere, keep the following in mind:
- While the logging tries to censor your passwords/passphrases/..., double check!
Maybe you also want to censor out e.g. the hostname/IP you're connecting to.
- If you want to report an issue regarding authentication or something else that
seems to be more of an issue with the actual SSH2 connection, it might be handy
to reconnect with this added to your User Settings (settings.json) first:
"sshfs.flags": [ "DEBUG_SSH2" ],
This will (for new connections) also enable internal SSH2 logging.
`);