diff --git a/.vscode/settings.json b/.vscode/settings.json index 3c64ad7..ee1f224 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,5 +25,6 @@ "**/.pnp.*": true }, "typescript.enablePromptUseWorkspaceTsdk": true, + "typescript.preferences.quoteStyle": "single", "prettier.prettierPath": ".yarn/sdks/prettier/index.js" } diff --git a/CHANGELOG.md b/CHANGELOG.md index dcaf03c..27b6b66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/config.ts b/src/config.ts index d952050..c9e4e93 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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 { - if (list === undefined) return {}; - if (!Array.isArray(list)) throw new Error(`Expected string array for flags, but got: ${list}`); - const scope: Record = {}; - 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 = [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 = {}; -function calculateFlags(): Record { - const flags: Record = {}; - const config = vscode.workspace.getConfiguration('sshfs').inspect('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 { - 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 { - 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!`); -} diff --git a/src/connect.ts b/src/connect.ts index ff237b9..41f4f69 100644 --- a/src/connect.ts +++ b/src/connect.ts @@ -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'; diff --git a/src/connection.ts b/src/connection.ts index 73f3d3b..becd604 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -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'; diff --git a/src/fileSystemRouter.ts b/src/fileSystemRouter.ts index 489f806..ac15ed2 100644 --- a/src/fileSystemRouter.ts +++ b/src/fileSystemRouter.ts @@ -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'; diff --git a/src/flags.ts b/src/flags.ts new file mode 100644 index 0000000..637fd47 --- /dev/null +++ b/src/flags.ts @@ -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 { + if (list === undefined) + return {}; + if (!Array.isArray(list)) + throw new Error(`Expected string array for flags, but got: ${list}`); + const scope: Record = {}; + 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 = [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 = {}; +function calculateFlags(): Record { + const flags: Record = {}; + const config = vscode.workspace.getConfiguration('sshfs').inspect('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 { + 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 { + 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!`); +} diff --git a/src/manager.ts b/src/manager.ts index f2b1697..7f9b6ff 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -2,7 +2,8 @@ import type { FileSystemConfig } from 'common/fileSystemConfig'; import type { Navigation } from 'common/webviewMessages'; import * as vscode from 'vscode'; -import { getConfig, getFlagBoolean, loadConfigsRaw } from './config'; +import { getConfig, loadConfigs, LOADING_CONFIGS } from './config'; +import { getFlagBoolean } from './flags'; import { Connection, ConnectionManager } from './connection'; import { Logging, LOGGING_NO_STACKTRACE } from './logging'; import { isSSHPseudoTerminal, replaceVariables, replaceVariablesRecursive } from './pseudoTerminal'; diff --git a/src/pseudoTerminal.ts b/src/pseudoTerminal.ts index 93f0e47..5debc33 100644 --- a/src/pseudoTerminal.ts +++ b/src/pseudoTerminal.ts @@ -1,10 +1,10 @@ -import type { EnvironmentVariable, FileSystemConfig } from "common/fileSystemConfig"; +import type { EnvironmentVariable, FileSystemConfig } from 'common/fileSystemConfig'; import * as path from 'path'; -import type { ClientChannel, PseudoTtyOptions } from "ssh2"; -import * as vscode from "vscode"; -import { getFlagBoolean } from './config'; +import type { ClientChannel, PseudoTtyOptions } from 'ssh2'; +import * as vscode from 'vscode'; +import { getFlagBoolean } from './flags'; import type { Connection } from './connection'; -import { Logging, LOGGING_NO_STACKTRACE } from "./logging"; +import { Logging, LOGGING_NO_STACKTRACE } from './logging'; import { environmentToExportString, joinCommands, mergeEnvironment, toPromise } from './utils'; const [HEIGHT, WIDTH] = [480, 640]; diff --git a/src/shellConfig.ts b/src/shellConfig.ts index be0af75..a59e69a 100644 --- a/src/shellConfig.ts +++ b/src/shellConfig.ts @@ -1,8 +1,8 @@ import { posix as path } from 'path'; -import type { Client, ClientChannel, SFTP } from "ssh2"; +import type { Client, ClientChannel, SFTP } from 'ssh2'; import type { Connection } from './connection'; -import { Logger, Logging, LOGGING_NO_STACKTRACE } from "./logging"; -import { toPromise } from "./utils"; +import { Logger, Logging, LOGGING_NO_STACKTRACE } from './logging'; +import { toPromise } from './utils'; const SCRIPT_COMMAND_CODE = `#!/bin/sh if [ "$#" -ne 1 ] || [ $1 = "help" ] || [ $1 = "--help" ] || [ $1 = "-h" ] || [ $1 = "-?" ]; then diff --git a/src/sshFileSystem.ts b/src/sshFileSystem.ts index cdaa732..e2461c5 100644 --- a/src/sshFileSystem.ts +++ b/src/sshFileSystem.ts @@ -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 { FlagValue, getFlag, subscribeToGlobalFlags } from './config'; +import { FlagValue, getFlag, subscribeToGlobalFlags } from './flags'; import { Logger, Logging, LOGGING_NO_STACKTRACE, LOGGING_SINGLE_LINE_STACKTRACE, withStacktraceOffset } from './logging'; import { toPromise } from './utils'; diff --git a/webview/src/tests/redux.tsx b/webview/src/tests/redux.tsx index 0fa5fdf..33c60a9 100644 --- a/webview/src/tests/redux.tsx +++ b/webview/src/tests/redux.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { connect } from "../redux"; +import { connect } from '../redux'; function nop(...args: any) { return;