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 export let DEBUG: boolean = false; export function setDebug(debug: boolean) { console.warn(`[vscode-sshfs] Debug mode set to ${debug}`); DEBUG = debug; if (!debug) return; import('source-map-support/register').catch(e => console.warn('Could not register 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; /** 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 = { callStacktrace: 0 }; export const LOGGING_SINGLE_LINE_STACKTRACE: Partial = { callStacktrace: 1 }; export type LoggerDefaultLevels = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR'; class Logger { protected parent?: Logger; protected stack?: string; protected defaultLoggingOptions: LoggingOptions = { reportedFromLevel: 0, callStacktrace: 0, callStacktraceOffset: 0, maxErrorStack: 0, }; public overriddenTypeOptions: { [type in LoggerDefaultLevels]?: Partial } = { 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 || ''; } } 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) { type = type.toUpperCase(); const options: LoggingOptions = { ...this.defaultLoggingOptions, ...this.overriddenTypeOptions[type], ...partialOptions }; // Format errors with stacktraces to display the JSON and the stacktrace if needed if (message instanceof Error && message.stack) { let msg = message.message; try { msg += `\nJSON: ${JSON.stringify(message)}`; } finally { } const { maxErrorStack } = options; if (message.stack && maxErrorStack) { let { stack } = message; if (maxErrorStack > 0) { stack = stack.split(/\n/g).slice(0, maxErrorStack).join('\n'); } msg += '\n' + stack; } 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') : ''; 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 = {}) { this.print('DEBUG', message, options); } public info(message: string, options: Partial = {}) { this.print('INFO', message, options); } public warning(message: string, options: Partial = {}) { this.print('WARNING', message, options); } public error(message: string | Error, options: Partial = {}) { this.print('ERROR', message, options); } } export type { Logger }; export function withStacktraceOffset(amount: number = 1, options: Partial = {}): Partial { return { ...options, callStacktraceOffset: (options.callStacktraceOffset || 0) + amount }; } export interface CensoredFileSystemConfig extends Omit { sock?: string; _calculated?: CensoredFileSystemConfig; } export function censorConfig(config: FileSystemConfig): CensoredFileSystemConfig { return { ...config, password: typeof config.password === 'string' ? '' : config.password, passphrase: typeof config.passphrase === 'string' ? '' : config.passphrase, privateKey: config.privateKey instanceof Buffer ? `Buffer(${config.privateKey.length})` : config.privateKey, sock: config.sock ? '' : 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');