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; /** 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 }; function hasPromiseCause(error: Error): error is Error & { promiseCause: string } { return typeof (error as any).promiseCause === 'string'; } 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 { const json = JSON.stringify(message); if (json !== '{}') msg += `\nJSON: ${json}`; } 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'); } 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; } 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 | Error, 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'); 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. `);