From 900d3a935416d010db809fa4a9831bae545a6e21 Mon Sep 17 00:00:00 2001 From: "minoru.nagasawa" Date: Fri, 3 Jan 2025 09:46:49 +0900 Subject: [PATCH 1/6] Add encoding support via iconv-lite --- .yarn/yarn.lock | 3 ++- common/src/fileSystemConfig.ts | 2 ++ package.json | 1 + src/pseudoTerminal.ts | 28 +++++++++++++++++++++++++--- 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/.yarn/yarn.lock b/.yarn/yarn.lock index 404b909..3b850b7 100644 --- a/.yarn/yarn.lock +++ b/.yarn/yarn.lock @@ -5703,7 +5703,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:^0.6.2": +"iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -9560,6 +9560,7 @@ __metadata: "@vscode/vsce": ^2.18.0 common: "workspace:*" event-stream: ^4.0.1 + iconv-lite: ^0.6.3 jsonc-parser: ^3.2.0 prettier: ^2.6.2 semver: ^7.3.5 diff --git a/common/src/fileSystemConfig.ts b/common/src/fileSystemConfig.ts index d5db4b7..cf7b719 100644 --- a/common/src/fileSystemConfig.ts +++ b/common/src/fileSystemConfig.ts @@ -117,6 +117,8 @@ export interface FileSystemConfig extends ConnectConfig { instantConnection?: boolean; /** List of special flags to enable/disable certain fixes/features. Flags are usually used for issues or beta testing. Flags can disappear/change anytime! */ flags?: string[]; + /** Specifies the character encoding used for the SSH terminal. If undefined or an unsupported by iconv-lite, UTF-8 will be used */ + encoding?: string; /** Internal property saying where this config comes from. Undefined if this config is merged or something */ _location?: ConfigLocation; /** Internal property keeping track of where this config comes from (including merges) */ diff --git a/package.json b/package.json index 1629798..b93dc33 100644 --- a/package.json +++ b/package.json @@ -429,6 +429,7 @@ "dependencies": { "common": "workspace:*", "event-stream": "^4.0.1", + "iconv-lite": "^0.6.3", "jsonc-parser": "^3.2.0", "semver": "^7.3.5", "socks": "^2.2.0", diff --git a/src/pseudoTerminal.ts b/src/pseudoTerminal.ts index 5debc33..176793d 100644 --- a/src/pseudoTerminal.ts +++ b/src/pseudoTerminal.ts @@ -2,6 +2,7 @@ import type { EnvironmentVariable, FileSystemConfig } from 'common/fileSystemCon import * as path from 'path'; import type { ClientChannel, PseudoTtyOptions } from 'ssh2'; import * as vscode from 'vscode'; +import * as iconv from 'iconv-lite'; import { getFlagBoolean } from './flags'; import type { Connection } from './connection'; import { Logging, LOGGING_NO_STACKTRACE } from './logging'; @@ -142,6 +143,27 @@ export async function createTerminal(options: TerminalOptions): Promise(); const onDidOpen = new vscode.EventEmitter(); let terminal: vscode.Terminal | undefined; + + // Encodes user input (originally UTF-8 in JS string) into the remote encoding, if configured. + // Returns a Buffer if encoding is valid, otherwise returns the original string. + function encodeTerminalInput(data: string) : Buffer | string { + const encoding = actualConfig.encoding; + if (!encoding || !iconv.encodingExists(encoding)) { + return data; + } + return iconv.encode(data, encoding); + } + + // Decodes data received from the remote side (as Buffer) into a string using the configured encoding. + // If encoding is not set or invalid, defaults to data.toString() (UTF-8). + function decodeTerminalOutput(data: Buffer) : string { + const encoding = actualConfig.encoding; + if (!encoding || !iconv.encodingExists(encoding)) { + return data.toString(); + } + return iconv.decode(data, encoding); + } + // Won't actually open the remote terminal until pseudo.open(dims) is called const pseudo: SSHPseudoTerminal = { status: 'opening', @@ -241,8 +263,8 @@ export async function createTerminal(options: TerminalOptions): Promise onDidWrite.fire(chunk.toString())); - channel.stderr!.on('data', chunk => onDidWrite.fire(chunk.toString())); + channel.on('data', chunk => onDidWrite.fire(decodeTerminalOutput(chunk))); + channel.stderr!.on('data', chunk => onDidWrite.fire(decodeTerminalOutput(chunk))); // TODO: ^ Keep track of stdout's color, switch to red, output, then switch back? } catch (e) { Logging.error`Error starting SSH terminal:\n${e}`; @@ -264,7 +286,7 @@ export async function createTerminal(options: TerminalOptions): Promise Date: Fri, 3 Jan 2025 15:27:11 +0900 Subject: [PATCH 2/6] Refactor terminal I/O encoding and decoding: --- src/pseudoTerminal.ts | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/pseudoTerminal.ts b/src/pseudoTerminal.ts index 176793d..1c76112 100644 --- a/src/pseudoTerminal.ts +++ b/src/pseudoTerminal.ts @@ -146,22 +146,19 @@ export async function createTerminal(options: TerminalOptions): Promise Buffer | string; + // Decodes data received from the remote side (as Buffer) into a string using the configured encoding. // If encoding is not set or invalid, defaults to data.toString() (UTF-8). - function decodeTerminalOutput(data: Buffer) : string { - const encoding = actualConfig.encoding; - if (!encoding || !iconv.encodingExists(encoding)) { - return data.toString(); - } - return iconv.decode(data, encoding); + let decodeTerminalOutput: (data: Buffer) => string; + + const encoding = actualConfig.encoding; + if (encoding && iconv.encodingExists(encoding)) { + encodeTerminalInput = (data: string) => iconv.encode(data, encoding); + decodeTerminalOutput = (data: Buffer) => iconv.decode(data, encoding); + } else { + encodeTerminalInput = (data: string) => data; + decodeTerminalOutput = (data: Buffer) => data.toString(); } // Won't actually open the remote terminal until pseudo.open(dims) is called From 43e0909f400f2c18cad9ae471b17dd020ca209d9 Mon Sep 17 00:00:00 2001 From: "minoru.nagasawa" Date: Fri, 3 Jan 2025 16:36:16 +0900 Subject: [PATCH 3/6] Add 'Encoding' field for SSH config --- webview/src/ConfigEditor/fields.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/webview/src/ConfigEditor/fields.tsx b/webview/src/ConfigEditor/fields.tsx index 2cabbc8..773d861 100644 --- a/webview/src/ConfigEditor/fields.tsx +++ b/webview/src/ConfigEditor/fields.tsx @@ -169,9 +169,16 @@ export function taskCommand(config: FileSystemConfig, onChange: FSCChanged<'task return } +export function encoding(config: FileSystemConfig, onChange: FSCChanged<'encoding'>): React.ReactElement { + const callback = (newValue?: string) => onChange('encoding', newValue); + const description = (<>Text encoding used for terminal input/output. For a list of supported encodings, see iconv-lite wiki.); + const values = ['utf8', 'iso-8859-1', 'Shift_JIS', 'EUC-JP', 'EUC-KR']; + return +} + export type FieldFactory = (config: FileSystemConfig, onChange: FSCChanged, onChangeMultiple: FSCChangedMultiple) => React.ReactElement | null; export const FIELDS: FieldFactory[] = [ name, label, group, merge, extend, putty, host, port, root, agent, username, password, privateKeyPath, passphrase, - newFileMode, agentForward, sftpCommand, sftpSudo, terminalCommand, taskCommand, + newFileMode, agentForward, sftpCommand, sftpSudo, terminalCommand, taskCommand, encoding, PROXY_FIELD]; From bf0b813fbc2a73a0db98d224b57b9e631c2aba34 Mon Sep 17 00:00:00 2001 From: "minoru.nagasawa" Date: Fri, 3 Jan 2025 16:41:54 +0900 Subject: [PATCH 4/6] Remove trailing period from description --- webview/src/ConfigEditor/fields.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webview/src/ConfigEditor/fields.tsx b/webview/src/ConfigEditor/fields.tsx index 773d861..5c2840b 100644 --- a/webview/src/ConfigEditor/fields.tsx +++ b/webview/src/ConfigEditor/fields.tsx @@ -171,7 +171,7 @@ export function taskCommand(config: FileSystemConfig, onChange: FSCChanged<'task export function encoding(config: FileSystemConfig, onChange: FSCChanged<'encoding'>): React.ReactElement { const callback = (newValue?: string) => onChange('encoding', newValue); - const description = (<>Text encoding used for terminal input/output. For a list of supported encodings, see iconv-lite wiki.); + const description = (<>Text encoding used for terminal input/output. For a list of supported encodings, see iconv-lite wiki); const values = ['utf8', 'iso-8859-1', 'Shift_JIS', 'EUC-JP', 'EUC-KR']; return } From 1a4a2e76c8b11290cb63c63293f542324a5e2627 Mon Sep 17 00:00:00 2001 From: "minoru.nagasawa" Date: Fri, 3 Jan 2025 16:45:36 +0900 Subject: [PATCH 5/6] Switch `description` from `string` to `React.ReactNode` to allow adding URL links --- webview/src/FieldTypes/base.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webview/src/FieldTypes/base.tsx b/webview/src/FieldTypes/base.tsx index 8c3d315..89bd7f5 100644 --- a/webview/src/FieldTypes/base.tsx +++ b/webview/src/FieldTypes/base.tsx @@ -4,7 +4,7 @@ import './index.css'; export interface Props { label?: string; - description?: string; + description?: React.ReactNode; value: T; optional?: boolean; group?: FieldGroup; From b975c9ddd8656043294b8ebd0c789c01dfb6728d Mon Sep 17 00:00:00 2001 From: Kelvin Schoofs Date: Sun, 9 Feb 2025 00:46:27 +0100 Subject: [PATCH 6/6] Resolve merge conflicts and improve code for pull request #431 --- common/src/fileSystemConfig.ts | 2 +- src/pseudoTerminal.ts | 36 ++++++++++++++-------------------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/common/src/fileSystemConfig.ts b/common/src/fileSystemConfig.ts index cf7b719..2ee1a11 100644 --- a/common/src/fileSystemConfig.ts +++ b/common/src/fileSystemConfig.ts @@ -117,7 +117,7 @@ export interface FileSystemConfig extends ConnectConfig { instantConnection?: boolean; /** List of special flags to enable/disable certain fixes/features. Flags are usually used for issues or beta testing. Flags can disappear/change anytime! */ flags?: string[]; - /** Specifies the character encoding used for the SSH terminal. If undefined or an unsupported by iconv-lite, UTF-8 will be used */ + /** Specifies the character encoding used for the SSH terminal. If undefined, UTF-8 will be used */ encoding?: string; /** Internal property saying where this config comes from. Undefined if this config is merged or something */ _location?: ConfigLocation; diff --git a/src/pseudoTerminal.ts b/src/pseudoTerminal.ts index 1c76112..9259a20 100644 --- a/src/pseudoTerminal.ts +++ b/src/pseudoTerminal.ts @@ -2,9 +2,8 @@ import type { EnvironmentVariable, FileSystemConfig } from 'common/fileSystemCon import * as path from 'path'; import type { ClientChannel, PseudoTtyOptions } from 'ssh2'; import * as vscode from 'vscode'; -import * as iconv from 'iconv-lite'; -import { getFlagBoolean } from './flags'; import type { Connection } from './connection'; +import { getFlagBoolean } from './flags'; import { Logging, LOGGING_NO_STACKTRACE } from './logging'; import { environmentToExportString, joinCommands, mergeEnvironment, toPromise } from './utils'; @@ -136,6 +135,16 @@ export async function replaceVariablesRecursive(object: T, handler: (value: s return object; } +async function getEncodingHandlers(encoding?: string): Promise<[encode: (data: string) => Buffer, decode: (data: Buffer) => string]> { + if (encoding) { + const iconv = await import('iconv-lite'); + if (!iconv.encodingExists(encoding)) + throw new Error(`Unknown character encoding '${encoding}'`); + return [data => iconv.encode(data, encoding), data => iconv.decode(data, encoding)]; + } + return [data => Buffer.from(data, 'utf-8'), data => data.toString('utf-8')]; +} + export async function createTerminal(options: TerminalOptions): Promise { const { connection } = options; const { actualConfig, client, shellConfig } = connection; @@ -144,22 +153,7 @@ export async function createTerminal(options: TerminalOptions): Promise(); let terminal: vscode.Terminal | undefined; - // Encodes user input (originally UTF-8 in JS string) into the remote encoding, if configured. - // Returns a Buffer if encoding is valid, otherwise returns the original string. - let encodeTerminalInput: (data: string) => Buffer | string; - - // Decodes data received from the remote side (as Buffer) into a string using the configured encoding. - // If encoding is not set or invalid, defaults to data.toString() (UTF-8). - let decodeTerminalOutput: (data: Buffer) => string; - - const encoding = actualConfig.encoding; - if (encoding && iconv.encodingExists(encoding)) { - encodeTerminalInput = (data: string) => iconv.encode(data, encoding); - decodeTerminalOutput = (data: Buffer) => iconv.decode(data, encoding); - } else { - encodeTerminalInput = (data: string) => data; - decodeTerminalOutput = (data: Buffer) => data.toString(); - } + const [encodeInput, decodeOutput] = await getEncodingHandlers(actualConfig.encoding); // Won't actually open the remote terminal until pseudo.open(dims) is called const pseudo: SSHPseudoTerminal = { @@ -260,8 +254,8 @@ export async function createTerminal(options: TerminalOptions): Promise onDidWrite.fire(decodeTerminalOutput(chunk))); - channel.stderr!.on('data', chunk => onDidWrite.fire(decodeTerminalOutput(chunk))); + channel.on('data', chunk => onDidWrite.fire(decodeOutput(chunk))); + channel.stderr!.on('data', chunk => onDidWrite.fire(decodeOutput(chunk))); // TODO: ^ Keep track of stdout's color, switch to red, output, then switch back? } catch (e) { Logging.error`Error starting SSH terminal:\n${e}`; @@ -283,7 +277,7 @@ export async function createTerminal(options: TerminalOptions): Promise