Skip to content

Commit

Permalink
Improve DEBUG_FS and FS_NOTIFY_ERRORS flags for logging (#341)
Browse files Browse the repository at this point in the history
  • Loading branch information
SchoofsKelvin committed Jun 2, 2022
1 parent 7d59992 commit 20cf037
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 28 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
- Similar to `DEBUG_FS` this is mostly meant for internal debugging or when useful for user-reported issues
- This flag will also auto-update when it changes in global flags.
- This is a singleton flag and thus unaffected by overriding it in your SSH FS configs
- Improved the above `DEBUG_FS` flag and refactored the already-existing `FS_NOTIFY_ERRORS` flag (#341)
- The `FS_NOTIFY_ERRORS` flag will auto-update when it changes in global flags, unless it's overriden in your SSH FS config
- The `FS_NOTIFY_ERRORS` flag is now a string representing a comma-separated list instead of just a boolean
- While disabled by default for older VS Code versions, starting from VS Code 1.56.0 the default is `write`
- The `write` flag will show a notification should an error happen for a "write" operation
- Write operations are: `createDirectory`, `writeFile`, `delete`, and `rename`
- Since `readDirectory`, `readFile` and `stat` are disabled by default, it should prevent extension detection spam (see #341)

## v1.25.0 (2022-06-01)

Expand Down
16 changes: 10 additions & 6 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,19 +388,23 @@ function parseFlagList(list: string[] | undefined, origin: string): Record<strin
- Enables debug logging for the remote command terminal (thus useless if REMOTE_COMMANDS isn't true)
DEBUG_FS (string) (default='')
- A comma-separated list of debug flags for logging errors in the sshFileSystem
- The presence of `ignoredmissing` will log `FileNotFound` that got ignored
- The presence of `showignored` will log `FileNotFound` that got ignored
- The presence of `disableignored` will make the code ignore nothing (making `showignored` useless)
- The presence of `minimal` will log all errors as single lines, but not `FileNotFound`
- The presence of `full` is the same as `minimal` but with full stacktraces
- The presence of `missing` will log `FileNotFound` errors in `minimal` and `full` (except `ignoredmissing` ones)
- The presence of `converted` will log the resulting converted errors (if required and successful)
- The presence of `all` enables all of the above (similar to `ignoredmissing,full,missing,converted,reads`)
- The presence of `all` enables all of the above except `disableignored` (similar to `showignored,full,converted`)
DEBUG_FSR (string) (default='', global)
- A comma-separated list of method names to enable logging for in the FileSystemRouter
- The presence of `all` is equal to `stat,readDirectory,createDirectory,readFile,writeFile,delete,rename`
- The router logs handles `ssh://`, and will even log operations to non-existing configurations/connections
FS_NOTIFY_ERRORS (boolean) (default=false)
- Enables displaying error notifications when a file system operation fails and isn't automatically ignored
- Automatically enabled VS Code 1.56 and later (see issue #282)
FS_NOTIFY_ERRORS (string) (default='')
- A comma-separated list of operations to display notifications for should they error
- Mind that `FileNotFound` errors for ignored paths are always ignored, except with `DEBUG_FS=showignored`
- The presence of `all` will show notification for every operation
- The presence of `write` is equal to `createDirectory,writeFile,delete,rename`
- Besides those provided by `write`, there's also `readDirectory`, `readFile` and `stat`
- Automatically set to `write` for VS Code 1.56 and later (see issue #282)
*/
export type FlagValue = string | boolean | null;
export type FlagCombo<V extends FlagValue = FlagValue> = [value: V, origin: string];
Expand Down
57 changes: 35 additions & 22 deletions src/sshFileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { FileSystemConfig } from 'common/fileSystemConfig';
import * as path from 'path';
import type * as ssh2 from 'ssh2';
import * as vscode from 'vscode';
import { getFlag, getFlagBoolean, subscribeToGlobalFlags } from './config';
import { FlagValue, getFlag, subscribeToGlobalFlags } from './config';
import { Logger, Logging, LOGGING_NO_STACKTRACE, LOGGING_SINGLE_LINE_STACKTRACE, withStacktraceOffset } from './logging';
import { toPromise } from './utils';

Expand Down Expand Up @@ -35,10 +35,15 @@ function shouldIgnoreNotFound(target: string) {
return false;
}

const DEBUG_NOTIFY_FLAGS: Record<string, string[] | undefined> = {};
DEBUG_NOTIFY_FLAGS.write = ['createdirectory', 'writefile', 'delete', 'rename'];
DEBUG_NOTIFY_FLAGS.all = [...DEBUG_NOTIFY_FLAGS.write, 'readdirectory', 'readfile', 'stat'];

export class SSHFileSystem implements vscode.FileSystemProvider {
protected onCloseEmitter = new vscode.EventEmitter<void>();
protected onDidChangeFileEmitter = new vscode.EventEmitter<vscode.FileChangeEvent[]>();
protected debugFlags: string[];
protected notifyErrorFlags: string[];
public closed = false;
public closing = false;
public copy = undefined;
Expand All @@ -50,8 +55,17 @@ export class SSHFileSystem implements vscode.FileSystemProvider {
this.sftp.on('end', () => (this.closed = true, this.onCloseEmitter.fire()));
this.logging.info('SSHFileSystem created');
const subscription = subscribeToGlobalFlags(() => {
// DEBUG_FS flag, with support for an 'all' alias
this.debugFlags = `${getFlag('DEBUG_FS', this.config.flags)?.[0] || ''}`.toLowerCase().split(/,\s*|\s+/g);
if (this.debugFlags.includes('all')) this.debugFlags.push('ignoredmissing', 'full', 'missing', 'converted');
if (this.debugFlags.includes('all')) this.debugFlags.push('showignored', 'full', 'converted');
// FS_NOTIFY_ERRORS flag, with support for a 'write' and 'all' alias, defined in DEBUG_NOTIFY_FLAGS
let notifyErrorFlag: FlagValue = (getFlag('FS_NOTIFY_ERRORS', this.config.flags) || ['write'])[0];
if (notifyErrorFlag === true) notifyErrorFlag = 'all'; // Flag used to be a boolean flag in v1.25.0 and earlier
this.notifyErrorFlags = (typeof notifyErrorFlag === 'string' ? notifyErrorFlag.toLowerCase().split(/,\s*|\s+/g) : []);
for (const flag of this.notifyErrorFlags) {
const alias = DEBUG_NOTIFY_FLAGS[flag];
if (alias) this.notifyErrorFlags.push(...alias);
}
});
this.onClose(() => subscription.dispose());
}
Expand All @@ -66,7 +80,7 @@ export class SSHFileSystem implements vscode.FileSystemProvider {
}
public async stat(uri: vscode.Uri): Promise<vscode.FileStat> {
const stat = await toPromise<ssh2.sftp.Stats>(cb => this.sftp.stat(uri.path, cb))
.catch(e => this.handleError(uri, e, true, true) as never);
.catch(e => this.handleError('stat', uri, e, true) as never);
const { mtime = 0, size = 0 } = stat;
let type = vscode.FileType.Unknown;
// tslint:disable no-bitwise */
Expand All @@ -81,7 +95,7 @@ export class SSHFileSystem implements vscode.FileSystemProvider {
}
public async readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> {
const entries = await toPromise<ssh2.sftp.DirectoryEntry[]>(cb => this.sftp.readdir(uri.path, cb))
.catch((e) => this.handleError(uri, e, true) as never);
.catch((e) => this.handleError('readDirectory', uri, e, true) as never);
return Promise.all(entries.map(async (file) => {
const furi = uri.with({ path: `${uri.path}${uri.path.endsWith('/') ? '' : '/'}${file.filename}` });
// Mode in octal representation is 120XXX for links, e.g. 120777
Expand All @@ -100,14 +114,14 @@ export class SSHFileSystem implements vscode.FileSystemProvider {
}));
}
public createDirectory(uri: vscode.Uri): void | Promise<void> {
return toPromise<void>(cb => this.sftp.mkdir(uri.path, cb)).catch(e => this.handleError(uri, e, true));
return toPromise<void>(cb => this.sftp.mkdir(uri.path, cb)).catch(e => this.handleError('createDirectory', uri, e, true));
}
public readFile(uri: vscode.Uri): Uint8Array | Promise<Uint8Array> {
return new Promise((resolve, reject) => {
const stream = this.sftp.createReadStream(uri.path, { autoClose: true });
const bufs = [];
stream.on('data', bufs.push.bind(bufs));
stream.on('error', e => this.handleError(uri, e, reject));
stream.on('error', e => this.handleError('readFile', uri, e, reject));
stream.on('close', () => {
resolve(new Uint8Array(Buffer.concat(bufs)));
});
Expand All @@ -128,12 +142,12 @@ export class SSHFileSystem implements vscode.FileSystemProvider {
if (typeof mode !== 'number') mode = 0o664;
if (Number.isNaN(mode)) throw new Error(`Invalid umask '${this.config.newFileMode}'`);
} else {
this.handleError(uri, e);
this.handleError('writeFile', uri, e);
vscode.window.showWarningMessage(`Couldn't read the permissions for '${uri.path}', permissions might be overwritten`);
}
}
const stream = this.sftp.createWriteStream(uri.path, { mode, flags: 'w' });
stream.on('error', e => this.handleError(uri, e, reject));
stream.on('error', e => this.handleError('writeFile', uri, e, reject));
stream.end(content, () => {
this.onDidChangeFileEmitter.fire([{ uri, type: fileExists ? vscode.FileChangeType.Changed : vscode.FileChangeType.Created }]);
resolve();
Expand All @@ -146,13 +160,13 @@ export class SSHFileSystem implements vscode.FileSystemProvider {
// tslint:disable no-bitwise */
if (stats.type & (vscode.FileType.SymbolicLink | vscode.FileType.File)) {
return toPromise(cb => this.sftp.unlink(uri.path, cb))
.then(fireEvent).catch(e => this.handleError(uri, e, true));
.then(fireEvent).catch(e => this.handleError('delete', uri, e, true));
} else if ((stats.type & vscode.FileType.Directory) && options.recursive) {
return toPromise(cb => this.sftp.rmdir(uri.path, cb))
.then(fireEvent).catch(e => this.handleError(uri, e, true));
.then(fireEvent).catch(e => this.handleError('delete', uri, e, true));
}
return toPromise(cb => this.sftp.unlink(uri.path, cb))
.then(fireEvent).catch(e => this.handleError(uri, e, true));
.then(fireEvent).catch(e => this.handleError('delete', uri, e, true));
// tslint:enable no-bitwise */
}
public rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean; }): void | Promise<void> {
Expand All @@ -161,25 +175,25 @@ export class SSHFileSystem implements vscode.FileSystemProvider {
{ uri: oldUri, type: vscode.FileChangeType.Deleted },
{ uri: newUri, type: vscode.FileChangeType.Created }
]))
.catch(e => this.handleError(newUri, e, true));
.catch(e => this.handleError('rename', newUri, e, true));
}
// Helper function to handle/report errors with proper (and minimal) stacktraces and such
protected handleError(uri: vscode.Uri, e: Error & { code?: any }, doThrow: (boolean | ((error: any) => void)) = false, ignoreNotFound = false): any {
const ignore = e.code === 2 && [ignoreNotFound, shouldIgnoreNotFound(uri.path)];
if (ignore && ignore.includes(true)) {
protected handleError(method: string, uri: vscode.Uri, e: Error & { code?: any }, doThrow: (boolean | ((error: any) => void)) = false): any {
const ignore = e.code === 2 && [method === 'stat', shouldIgnoreNotFound(uri.path)];
if (ignore && ignore.includes(true) && !this.debugFlags.includes('disableignored')) {
e = vscode.FileSystemError.FileNotFound(uri);
// Whenever a workspace opens, VSCode (and extensions) (indirectly) stat a bunch of files
// (.vscode/tasks.json etc, .git/, node_modules for NodeJS, pom.xml for Maven, ...)
if (this.debugFlags.includes('ignoredmissing')) {
if (this.debugFlags.includes('showignored')) {
const flags = `${ignore[0] ? 'F' : ''}${ignore[1] ? 'A' : ''}`;
this.logging.debug(`Ignored (${flags}) FileNotFound error for: ${uri}`, LOGGING_NO_STACKTRACE);
this.logging.debug(`Ignored (${flags}) FileNotFound error for ${method}: ${uri}`, LOGGING_NO_STACKTRACE);
}
if (doThrow === true) throw e; else if (doThrow) return doThrow(e); else return;
}
else if (this.debugFlags.includes('full')) {
this.logging.debug.withOptions(LOGGING_HANDLE_ERROR)`Error in ${uri}: ${e}`;
this.logging.debug.withOptions(LOGGING_HANDLE_ERROR)`Error during ${method} ${uri}: ${e}`;
} else if (this.debugFlags.includes('minimal')) {
this.logging.debug.withOptions({ ...LOGGING_NO_STACKTRACE, maxErrorStack: 0 })`Error in ${uri}: ${e.name}: ${e.message}`;
this.logging.debug.withOptions({ ...LOGGING_NO_STACKTRACE, maxErrorStack: 0 })`Error during ${method} ${uri}: ${e.name}: ${e.message}`;
}
// Convert SSH2Stream error codes into VS Code errors
if (doThrow && typeof e.code === 'number') {
Expand All @@ -197,9 +211,8 @@ export class SSHFileSystem implements vscode.FileSystemProvider {
Logging.debug(`Error converted to: ${e}`);
}
// Display an error notification if the FS_ERROR_NOTIFICATION flag is enabled
const [flagCH] = getFlagBoolean('FS_NOTIFY_ERRORS', true, this.config.flags);
if (flagCH) {
vscode.window.showErrorMessage(`Error handling uri: ${uri}\n${e.message || e}`);
if (this.notifyErrorFlags.includes(method.toLowerCase())) {
vscode.window.showErrorMessage(`Error handling ${method} for: ${uri}\n${e.message || e}`);
}
if (doThrow === true) throw e;
if (doThrow) return doThrow(e);
Expand Down

0 comments on commit 20cf037

Please sign in to comment.