Skip to content

Commit

Permalink
Move flag system to separate file and fix import quotes
Browse files Browse the repository at this point in the history
  • Loading branch information
SchoofsKelvin committed Mar 25, 2023
1 parent 5721f1c commit 11b8f05
Show file tree
Hide file tree
Showing 12 changed files with 201 additions and 181 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@
"**/.pnp.*": true
},
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.preferences.quoteStyle": "single",
"prettier.prettierPath": ".yarn/sdks/prettier/index.js"
}
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
- Since `readDirectory`, `readFile` and `stat` are disabled by default, it should prevent extension detection spam (see #341)
- Added the `SHELL_CONFIG` flag to force a specific remote shell configuration (#331)

### Development changes
- Move the whole flag system from config.ts to flags.ts

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

### Major change
Expand Down
167 changes: 0 additions & 167 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,170 +342,3 @@ vscode.workspace.onDidChangeConfiguration(async (e) => {
// if (!e.affectsConfiguration('sshfs.configs')) return;
return loadConfigs();
});

function parseFlagList(list: string[] | undefined, origin: string): Record<string, FlagCombo> {
if (list === undefined) return {};
if (!Array.isArray(list)) throw new Error(`Expected string array for flags, but got: ${list}`);
const scope: Record<string, FlagCombo> = {};
for (const flag of list) {
let name: string = flag;
let value: FlagValue = null;
const eq = flag.indexOf('=');
if (eq !== -1) {
name = flag.substring(0, eq);
value = flag.substring(eq + 1);
} else if (flag.startsWith('+')) {
name = flag.substring(1);
value = true;
} else if (flag.startsWith('-')) {
name = flag.substring(1);
value = false;
}
name = name.toLocaleLowerCase();
if (name in scope) continue;
scope[name] = [value, origin];
}
return scope;
}

/* List of flags
DF-GE (boolean) (default=false)
- Disables the 'diffie-hellman-group-exchange' kex algorithm as a default option
- Originally for issue #239
- Automatically enabled for Electron v11.0, v11.1 and v11.2
DEBUG_SSH2 (boolean) (default=false)
- Enables debug logging in the ssh2 library (set at the start of each connection)
WINDOWS_COMMAND_SEPARATOR (boolean) (default=false)
- Makes it that commands are joined together using ` && ` instead of `; `
- Automatically enabled when the remote shell is detected to be PowerShell or Command Prompt (cmd.exe)
CHECK_HOME (boolean) (default=true)
- Determines whether we check if the home directory exists during `createFileSystem` in the Manager
- If `tryGetHome` fails while creating the connection, throw an error if this flag is set, otherwise default to `/`
REMOTE_COMMANDS (boolean) (default=false)
- Enables automatically launching a background command terminal during connection setup
- Enables attempting to inject a file to be sourced by the remote shells (which adds the `code` alias)
DEBUG_REMOTE_COMMANDS (boolean) (default=false)
- 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 `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 `converted` will log the resulting converted errors (if required and successful)
- 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 (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)
SHELL_CONFIG (string)
- Forces the use of a specific shell configuration. Check shellConfig.ts for possible values
- By default, when this flag is absent (or an empty or not a string), the extension will try to detect the correct type to use
*/
export type FlagValue = string | boolean | null;
export type FlagCombo<V extends FlagValue = FlagValue> = [value: V, origin: string];

const globalFlagsSubscribers = new Set<() => void>();
export function subscribeToGlobalFlags(listener: () => void): vscode.Disposable {
listener();
globalFlagsSubscribers.add(listener);
return new vscode.Disposable(() => globalFlagsSubscribers.delete(listener));
}

export const DEFAULT_FLAGS: string[] = [];
let cachedFlags: Record<string, FlagCombo> = {};
function calculateFlags(): Record<string, FlagCombo> {
const flags: Record<string, FlagCombo> = {};
const config = vscode.workspace.getConfiguration('sshfs').inspect<string[]>('flags');
if (!config) throw new Error(`Could not inspect "sshfs.flags" config field`);
const applyList = (list: string[] | undefined, origin: string) => Object.assign(flags, parseFlagList(list, origin));
applyList(DEFAULT_FLAGS, 'Built-in Default');
applyList(config.defaultValue, 'Default Settings');
// Electron v11 crashes for DiffieHellman GroupExchange, although it's fixed in 11.3.0
if ((process.versions as { electron?: string }).electron?.match(/^11\.(0|1|2)\./)) {
applyList(['+DF-GE'], 'Fix for issue #239')
}
// Starting with 1.56, FileSystemProvider errors aren't shown to the user and just silently fail
// https://github.com/SchoofsKelvin/vscode-sshfs/issues/282
if (semver.gte(vscode.version, '1.56.0')) {
applyList(['+FS_NOTIFY_ERRORS'], 'Fix for issue #282');
}
applyList(config.globalValue, 'Global Settings');
applyList(config.workspaceValue, 'Workspace Settings');
applyList(config.workspaceFolderValue, 'WorkspaceFolder Settings');
Logging.info`Calculated config flags: ${flags}`;
for (const listener of globalFlagsSubscribers) {
catchingPromise(listener).catch(e => Logging.error`onGlobalFlagsChanged listener errored: ${e}`);
}
return cachedFlags = flags;
}

vscode.workspace.onDidChangeConfiguration(event => {
if (event.affectsConfiguration('sshfs.flags')) calculateFlags();
});
calculateFlags();

/**
* Returns (a copy of the) global flags. Gets updated by ConfigurationChangeEvent events.
* In case `flags` is given, flags specified in this array will override global ones in the returned result.
* @param flags An optional array of flags to check before the global ones
*/
export function getFlags(flags?: string[]): Record<string, FlagCombo> {
return {
...cachedFlags,
...parseFlagList(flags, 'Override'),
};
}

/**
* Checks the `sshfs.flags` config (overridable by e.g. workspace settings).
* - Flag names are case-insensitive
* - If a flag appears twice, the first mention of it is used
* - If a flag appears as "NAME", `null` is returned
* - If a flag appears as "FLAG=VALUE", `VALUE` is returned as a string
* - If a flag appears as `+FLAG` (and no `=`), `true` is returned (as a boolean)
* - If a flag appears as `-FLAG` (and no `=`), `false` is returned (as a boolean)
* - If a flag is missing, `undefined` is returned (different from `null`!)
*
* For `undefined`, an actual `undefined` is returned. For all other cases, a FlagCombo
* is returned, e.g. "NAME" returns `[null, "someOrigin"]` and `"+F"` returns `[true, "someOrigin"]`
* @param target The name of the flag to look for
* @param flags An optional array of flags to check before the global ones
*/
export function getFlag(target: string, flags?: string[]): FlagCombo | undefined {
return getFlags(flags)[target.toLowerCase()];
}

/**
* Built on top of getFlag. Tries to convert the flag value to a boolean using these rules:
* - If the flag isn't present, `missingValue` is returned
* Although this probably means I'm using a flag that I never added to `DEFAULT_FLAGS`
* - Booleans are kept
* - `null` is counted as `true` (means a flag like "NAME" was present without any value or prefix)
* - Strings try to get converted in a case-insensitive way:
* - `true/t/yes/y` becomes true
* - `false/f/no/n` becomes false
* - All other strings result in an error
* @param target The name of the flag to look for
* @param defaultValue The value to return when no flag with the given name is present
* @param flags An optional array of flags to check before the global ones
* @returns The matching FlagCombo or `[missingValue, 'missing']` instead
*/
export function getFlagBoolean(target: string, missingValue: boolean, flags?: string[]): FlagCombo<boolean> {
const combo = getFlag(target, flags);
if (!combo) return [missingValue, 'missing'];
const [value, reason] = combo;
if (value == null) return [true, reason];
if (typeof value === 'boolean') return [value, reason];
const lower = value.toLowerCase();
if (lower === 'true' || lower === 't' || lower === 'yes' || lower === 'y') return [true, reason];
if (lower === 'false' || lower === 'f' || lower === 'no' || lower === 'n') return [false, reason];
throw new Error(`Could not convert '${value}' for flag '${target}' to a boolean!`);
}
3 changes: 2 additions & 1 deletion src/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { userInfo } from 'os';
import { Client, ClientChannel, ConnectConfig } from 'ssh2';
import { SFTP } from 'ssh2/lib/protocol/SFTP';
import * as vscode from 'vscode';
import { getConfig, getFlagBoolean } from './config';
import { getConfig } from './config';
import { getFlagBoolean } from './flags';
import { Logging } from './logging';
import type { PuttySession } from './putty';
import { toPromise, validatePort } from './utils';
Expand Down
3 changes: 2 additions & 1 deletion src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { posix as path } from 'path';
import * as readline from 'readline';
import type { Client, ClientChannel } from 'ssh2';
import * as vscode from 'vscode';
import { configMatches, getFlag, getFlagBoolean, loadConfigs } from './config';
import { configMatches, loadConfigs } from './config';
import { getFlag, getFlagBoolean } from './flags';
import { Logging, LOGGING_NO_STACKTRACE } from './logging';
import type { SSHPseudoTerminal } from './pseudoTerminal';
import { calculateShellConfig, KNOWN_SHELL_CONFIGS, ShellConfig, tryCommand, tryEcho } from './shellConfig';
Expand Down
2 changes: 1 addition & 1 deletion src/fileSystemRouter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as vscode from 'vscode';
import { getFlag, subscribeToGlobalFlags } from './config';
import { getFlag, subscribeToGlobalFlags } from './flags';
import { Logging } from './logging';
import type { Manager } from './manager';

Expand Down
180 changes: 180 additions & 0 deletions src/flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import * as semver from 'semver';
import * as vscode from 'vscode';
import { Logging } from './logging';
import { catchingPromise } from './utils';

/* List of flags
DF-GE (boolean) (default=false)
- Disables the 'diffie-hellman-group-exchange' kex algorithm as a default option
- Originally for issue #239
- Automatically enabled for Electron v11.0, v11.1 and v11.2
DEBUG_SSH2 (boolean) (default=false)
- Enables debug logging in the ssh2 library (set at the start of each connection)
WINDOWS_COMMAND_SEPARATOR (boolean) (default=false)
- Makes it that commands are joined together using ` && ` instead of `; `
- Automatically enabled when the remote shell is detected to be PowerShell or Command Prompt (cmd.exe)
CHECK_HOME (boolean) (default=true)
- Determines whether we check if the home directory exists during `createFileSystem` in the Manager
- If `tryGetHome` fails while creating the connection, throw an error if this flag is set, otherwise default to `/`
REMOTE_COMMANDS (boolean) (default=false)
- Enables automatically launching a background command terminal during connection setup
- Enables attempting to inject a file to be sourced by the remote shells (which adds the `code` alias)
DEBUG_REMOTE_COMMANDS (boolean) (default=false)
- 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 `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 `converted` will log the resulting converted errors (if required and successful)
- 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 (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)
SHELL_CONFIG (string)
- Forces the use of a specific shell configuration. Check shellConfig.ts for possible values
- By default, when this flag is absent (or an empty or not a string), the extension will try to detect the correct type to use
*/

function parseFlagList(list: string[] | undefined, origin: string): Record<string, FlagCombo> {
if (list === undefined)
return {};
if (!Array.isArray(list))
throw new Error(`Expected string array for flags, but got: ${list}`);
const scope: Record<string, FlagCombo> = {};
for (const flag of list) {
let name: string = flag;
let value: FlagValue = null;
const eq = flag.indexOf('=');
if (eq !== -1) {
name = flag.substring(0, eq);
value = flag.substring(eq + 1);
} else if (flag.startsWith('+')) {
name = flag.substring(1);
value = true;
} else if (flag.startsWith('-')) {
name = flag.substring(1);
value = false;
}
name = name.toLocaleLowerCase();
if (name in scope)
continue;
scope[name] = [value, origin];
}
return scope;
}

export type FlagValue = string | boolean | null;
export type FlagCombo<V extends FlagValue = FlagValue> = [value: V, origin: string];
const globalFlagsSubscribers = new Set<() => void>();
export function subscribeToGlobalFlags(listener: () => void): vscode.Disposable {
listener();
globalFlagsSubscribers.add(listener);
return new vscode.Disposable(() => globalFlagsSubscribers.delete(listener));
}

const DEFAULT_FLAGS: string[] = [];
let cachedFlags: Record<string, FlagCombo> = {};
function calculateFlags(): Record<string, FlagCombo> {
const flags: Record<string, FlagCombo> = {};
const config = vscode.workspace.getConfiguration('sshfs').inspect<string[]>('flags');
if (!config)
throw new Error(`Could not inspect "sshfs.flags" config field`);
const applyList = (list: string[] | undefined, origin: string) => Object.assign(flags, parseFlagList(list, origin));
applyList(DEFAULT_FLAGS, 'Built-in Default');
applyList(config.defaultValue, 'Default Settings');
// Electron v11 crashes for DiffieHellman GroupExchange, although it's fixed in 11.3.0
if ((process.versions as { electron?: string; }).electron?.match(/^11\.(0|1|2)\./)) {
applyList(['+DF-GE'], 'Fix for issue #239');
}
// Starting with 1.56, FileSystemProvider errors aren't shown to the user and just silently fail
// https://github.com/SchoofsKelvin/vscode-sshfs/issues/282
if (semver.gte(vscode.version, '1.56.0')) {
applyList(['+FS_NOTIFY_ERRORS'], 'Fix for issue #282');
}
applyList(config.globalValue, 'Global Settings');
applyList(config.workspaceValue, 'Workspace Settings');
applyList(config.workspaceFolderValue, 'WorkspaceFolder Settings');
Logging.info`Calculated config flags: ${flags}`;
for (const listener of globalFlagsSubscribers) {
catchingPromise(listener).catch(e => Logging.error`onGlobalFlagsChanged listener errored: ${e}`);
}
return cachedFlags = flags;
}
vscode.workspace.onDidChangeConfiguration(event => {
if (event.affectsConfiguration('sshfs.flags'))
calculateFlags();
});
calculateFlags();

/**
* Returns (a copy of the) global flags. Gets updated by ConfigurationChangeEvent events.
* In case `flags` is given, flags specified in this array will override global ones in the returned result.
* @param flags An optional array of flags to check before the global ones
*/
function getFlags(flags?: string[]): Record<string, FlagCombo> {
return {
...cachedFlags,
...parseFlagList(flags, 'Override'),
};
}

/**
* Checks the `sshfs.flags` config (overridable by e.g. workspace settings).
* - Flag names are case-insensitive
* - If a flag appears twice, the first mention of it is used
* - If a flag appears as "NAME", `null` is returned
* - If a flag appears as "FLAG=VALUE", `VALUE` is returned as a string
* - If a flag appears as `+FLAG` (and no `=`), `true` is returned (as a boolean)
* - If a flag appears as `-FLAG` (and no `=`), `false` is returned (as a boolean)
* - If a flag is missing, `undefined` is returned (different from `null`!)
*
* For `undefined`, an actual `undefined` is returned. For all other cases, a FlagCombo
* is returned, e.g. "NAME" returns `[null, "someOrigin"]` and `"+F"` returns `[true, "someOrigin"]`
* @param target The name of the flag to look for
* @param flags An optional array of flags to check before the global ones
*/
export function getFlag(target: string, flags?: string[]): FlagCombo | undefined {
return getFlags(flags)[target.toLowerCase()];
}

/**
* Built on top of getFlag. Tries to convert the flag value to a boolean using these rules:
* - If the flag isn't present, `missingValue` is returned
* Although this probably means I'm using a flag that I never added to `DEFAULT_FLAGS`
* - Booleans are kept
* - `null` is counted as `true` (means a flag like "NAME" was present without any value or prefix)
* - Strings try to get converted in a case-insensitive way:
* - `true/t/yes/y` becomes true
* - `false/f/no/n` becomes false
* - All other strings result in an error
* @param target The name of the flag to look for
* @param defaultValue The value to return when no flag with the given name is present
* @param flags An optional array of flags to check before the global ones
* @returns The matching FlagCombo or `[missingValue, 'missing']` instead
*/
export function getFlagBoolean(target: string, missingValue: boolean, flags?: string[]): FlagCombo<boolean> {
const combo = getFlag(target, flags);
if (!combo)
return [missingValue, 'missing'];
const [value, reason] = combo;
if (value == null)
return [true, reason];
if (typeof value === 'boolean')
return [value, reason];
const lower = value.toLowerCase();
if (lower === 'true' || lower === 't' || lower === 'yes' || lower === 'y')
return [true, reason];
if (lower === 'false' || lower === 'f' || lower === 'no' || lower === 'n')
return [false, reason];
throw new Error(`Could not convert '${value}' for flag '${target}' to a boolean!`);
}
Loading

0 comments on commit 11b8f05

Please sign in to comment.