Skip to content

Commit

Permalink
SSH Target Selector (#9760)
Browse files Browse the repository at this point in the history
* SSH Target Selector
  • Loading branch information
xisui-MSFT authored Sep 1, 2022
1 parent 466fb3b commit 03b3995
Show file tree
Hide file tree
Showing 21 changed files with 1,742 additions and 612 deletions.
973 changes: 555 additions & 418 deletions Extension/package.json

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions Extension/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@
"c_cpp.command.BuildAndRunFile.title": "Run C/C++ File",
"c_cpp.command.AddDebugConfiguration.title": "Add Debug Configuration",
"c_cpp.command.GenerateDoxygenComment.title": "Generate Doxygen Comment",
"c_cpp.command.addSshTarget.title": "Add SSH target",
"c_cpp.command.removeSshTarget.title": "Remove SSH target",
"c_cpp.command.setActiveSshTarget.title": "Set this SSH target as the active target",
"c_cpp.command.selectActiveSshTarget.title": "Select an active SSH target",
"c_cpp.command.selectSshTarget.title": "Select SSH target",
"c_cpp.command.activeSshTarget.title": "Get the active SSH target",
"c_cpp.command.refreshCppSshTargetsView.title": "Refresh",
"c_cpp.configuration.maxConcurrentThreads.markdownDescription": { "message": "The maximum number of concurrent threads to use for language service processing. The value is a hint and may not always be used. The default of `null` (empty) uses the number of logical processors available.", "comment": [ "Markdown text between `` should not be translated or localized (they represent literal text) and the capitalization, spacing, and punctuation (including the ``) should not be altered." ] },
"c_cpp.configuration.maxCachedProcesses.markdownDescription": { "message": "The maximum number of cached processes to use for language service processing. The default of `null` (empty) uses twice the number of logical processors available.", "comment": [ "Markdown text between `` should not be translated or localized (they represent literal text) and the capitalization, spacing, and punctuation (including the ``) should not be altered." ] },
"c_cpp.configuration.maxMemory.markdownDescription": { "message": "The maximum memory (in MB) available for language service processing. Fewer processes will be cached and run concurrently after this memory usage is exceeded. The default of `null` (empty) uses the system's free memory.", "comment": [ "Markdown text between `` should not be translated or localized (they represent literal text) and the capitalization, spacing, and punctuation (including the ``) should not be altered." ] },
Expand Down Expand Up @@ -232,6 +239,7 @@
"c_cpp.configuration.legacyCompilerArgsBehavior.markdownDescription": "Enable pre-v1.10.0 behavior for how shell escaping is handled in compiler arg settings. Shell escaping is no longer expected or supported by default in arg arrays starting in v1.10.0.",
"c_cpp.configuration.legacyCompilerArgsBehavior.deprecationMessage": "This setting is temporary to support transitioning to corrected behavior in v1.10.0.",
"c_cpp.contributes.views.cppReferencesView.title": "C/C++: Other references results",
"c_cpp.contributes.views.sshTargetsView.title": { "message": "Cpptools: SSH targets", "comment": [ "Do not localize `Cpptools`." ] },
"c_cpp.contributes.viewsWelcome.contents": { "message": "To learn more about launch.json, see [Configuring C/C++ debugging](https://code.visualstudio.com/docs/cpp/launch-json-reference).", "comment": [ "Markdown text between () should not be altered: https://en.wikipedia.org/wiki/Markdown" ] },
"c_cpp.configuration.debugShortcut.description": "Show the \"Run and Debug\" play button and \"Add Debug Configuration\" gear in the editor title bar for C++ files.",
"c_cpp.debuggers.pipeTransport.description": "When present, this tells the debugger to connect to a remote computer using another executable as a pipe that will relay standard input/output between VS Code and the MI-enabled debugger backend executable (such as gdb).",
Expand Down
4 changes: 2 additions & 2 deletions Extension/src/Debugger/configurationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1030,7 +1030,7 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
logger.getOutputChannelLogger().showErrorMessage(localize('missing.properties.copyFile', '"host", "files", and "targetDir" are required in {0} steps.', isScp ? 'SCP' : 'rsync'));
return false;
}
const host: util.ISshHostInfo = { hostName: step.host.hostName, user: step.host.user, port: step.host.port };
const host: util.ISshHostInfo = util.isString(step.host) ? { hostName: step.host } : { hostName: step.host.hostName, user: step.host.user, port: step.host.port };
const jumpHosts: util.ISshHostInfo[] = step.host.jumpHosts;
let files: vscode.Uri[] = [];
if (util.isString(step.files)) {
Expand Down Expand Up @@ -1061,7 +1061,7 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
logger.getOutputChannelLogger().showErrorMessage(localize('missing.properties.ssh', '"host" and "command" are required for ssh steps.'));
return false;
}
const host: util.ISshHostInfo = { hostName: step.host.hostName, user: step.host.user, port: step.host.port };
const host: util.ISshHostInfo = util.isString(step.host) ? { hostName: step.host } : { hostName: step.host.hostName, user: step.host.user, port: step.host.port };
const jumpHosts: util.ISshHostInfo[] = step.host.jumpHosts;
const localForwards: util.ISshLocalForwardInfo[] = step.host.localForwards;
const continueOn: string = step.continueOn;
Expand Down
91 changes: 90 additions & 1 deletion Extension/src/Debugger/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,22 @@ import { DebugConfigurationProvider, ConfigurationAssetProviderFactory, Configur
import { CppdbgDebugAdapterDescriptorFactory, CppvsdbgDebugAdapterDescriptorFactory } from './debugAdapterDescriptorFactory';
import { DebuggerType } from './configurations';
import * as nls from 'vscode-nls';
import { getActiveSshTarget, initializeSshTargets, selectSshTarget, SshTargetsProvider } from '../SSH/TargetsView/sshTargetsProvider';
import { addSshTarget, BaseNode, refreshCppSshTargetsView } from '../SSH/TargetsView/common';
import { setActiveSshTarget, TargetLeafNode } from '../SSH/TargetsView/targetNodes';
import { sshCommandToConfig } from '../SSH/sshCommandToConfig';
import { getSshConfiguration, getSshConfigurationFiles, writeSshConfiguration } from '../SSH/sshHosts';
import { pathAccessible } from '../common';
import * as fs from 'fs';
import { Configuration } from 'ssh-config';
import * as chokidar from 'chokidar';

// The extension deactivate method is asynchronous, so we handle the disposables ourselves instead of using extensionContext.subscriptions.
const disposables: vscode.Disposable[] = [];
const localize: nls.LocalizeFunc = nls.loadMessageBundle();

const fileWatchers: chokidar.FSWatcher[] = [];

export async function initialize(context: vscode.ExtensionContext): Promise<void> {
// Activate Process Picker Commands
const attachItemsProvider: AttachItemsProvider = NativeAttachItemsProviderFactory.Get();
Expand Down Expand Up @@ -64,9 +75,87 @@ export async function initialize(context: vscode.ExtensionContext): Promise<void
disposables.push(vscode.debug.registerDebugAdapterDescriptorFactory(DebuggerType.cppvsdbg , new CppvsdbgDebugAdapterDescriptorFactory(context)));
disposables.push(vscode.debug.registerDebugAdapterDescriptorFactory(DebuggerType.cppdbg, new CppdbgDebugAdapterDescriptorFactory(context)));

vscode.Disposable.from(...disposables);
// SSH Targets View
await initializeSshTargets();
const sshTargetsProvider: SshTargetsProvider = new SshTargetsProvider();
disposables.push(vscode.window.registerTreeDataProvider('CppSshTargetsView', sshTargetsProvider));
disposables.push(vscode.commands.registerCommand(addSshTarget, addSshTargetImpl));
disposables.push(vscode.commands.registerCommand('C_Cpp.removeSshTarget', removeSshTargetImpl));
disposables.push(vscode.commands.registerCommand(refreshCppSshTargetsView, (node?: BaseNode) => sshTargetsProvider.refresh(node)));
disposables.push(vscode.commands.registerCommand('C_Cpp.setActiveSshTarget', async (node: TargetLeafNode) => {
await setActiveSshTarget(node.name);
await vscode.commands.executeCommand(refreshCppSshTargetsView);
}));
disposables.push(vscode.commands.registerCommand('C_Cpp.selectSshTarget', selectSshTarget));
disposables.push(vscode.commands.registerCommand('C_Cpp.selectActiveSshTarget', async () => {
const name: string | undefined = await selectSshTarget();
if (name) {
await setActiveSshTarget(name);
await vscode.commands.executeCommand(refreshCppSshTargetsView);
}
}));
disposables.push(vscode.commands.registerCommand('C_Cpp.activeSshTarget', getActiveSshTarget));
disposables.push(sshTargetsProvider);
for (const sshConfig of getSshConfigurationFiles()) {
fileWatchers.push(chokidar.watch(sshConfig, {ignoreInitial: true})
.on('add', () => vscode.commands.executeCommand(refreshCppSshTargetsView))
.on('change', () => vscode.commands.executeCommand(refreshCppSshTargetsView))
.on('unlink', () => vscode.commands.executeCommand(refreshCppSshTargetsView)));
}
vscode.commands.executeCommand('setContext', 'showCppSshTargetsView', true);
}

export function dispose(): void {
// Don't wait
fileWatchers.forEach(watcher => watcher.close().then(() => {}, () => {}));
disposables.forEach(d => d.dispose());
}

async function addSshTargetImpl(): Promise<string> {
const name: string | undefined = await vscode.window.showInputBox({
title: localize('enter.ssh.target.name', 'Enter SSH Target Name'),
placeHolder: localize('ssh.target.name.place.holder', 'Example: `mySSHTarget`'),
ignoreFocusOut: true
});
if (name === undefined) {
// Cancelled
return '';
}

const command: string | undefined = await vscode.window.showInputBox({
title: localize('enter.ssh.connection.command', 'Enter SSH Connection Command'),
placeHolder: localize('ssh.connection.command.place.holder', 'Example: `ssh hello@microsoft.com -A`'),
ignoreFocusOut: true
});
if (!command) {
return '';
}

const newEntry: { [key: string]: string } = sshCommandToConfig(command, name);

const targetFile: string | undefined = await vscode.window.showQuickPick(getSshConfigurationFiles().filter(file => pathAccessible(file, fs.constants.W_OK)), { title: localize('select.ssh.config.file', 'Select an SSH configuration file') });
if (!targetFile) {
return '';
}

const parsedSshConfig: Configuration = await getSshConfiguration(targetFile, false);
parsedSshConfig.prepend(newEntry, true);
await writeSshConfiguration(targetFile, parsedSshConfig);

return name;
}

async function removeSshTargetImpl(node: TargetLeafNode): Promise<boolean> {
const labelYes: string = localize('yes', 'Yes');
const labelNo: string = localize('no', 'No');
const confirm: string | undefined = await vscode.window.showInformationMessage(localize('ssh.target.delete.confirmation', 'Are you sure you want to permanamtly delete "{0}"?', node.name), labelYes, labelNo);
if (!confirm || confirm === labelNo) {
return false;
}

const parsedSshConfig: Configuration = await getSshConfiguration(node.sshConfigHostInfo.file, false);
parsedSshConfig.remove({ Host: node.name });
await writeSshConfiguration(node.sshConfigHostInfo.file, parsedSshConfig);

return true;
}
40 changes: 39 additions & 1 deletion Extension/src/LanguageServer/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import * as yauzl from 'yauzl';
import { Readable } from 'stream';
import * as nls from 'vscode-nls';
import { CppBuildTaskProvider } from './cppBuildTaskProvider';
import { UpdateInsidersAccess } from '../main';

nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize: nls.LocalizeFunc = nls.loadMessageBundle();
Expand Down Expand Up @@ -1023,3 +1022,42 @@ export function getClients(): ClientCollection {
export function getActiveClient(): Client {
return clients.ActiveClient;
}

export function UpdateInsidersAccess(): void {
let installPrerelease: boolean = false;

// Only move them to the new prerelease mechanism if using updateChannel of Insiders.
const settings: CppSettings = new CppSettings();
const migratedInsiders: PersistentState<boolean> = new PersistentState<boolean>("CPP.migratedInsiders", false);
if (settings.updateChannel === "Insiders") {
// Don't do anything while the user has autoUpdate disabled, so we do not cause the extension to be updated.
if (!migratedInsiders.Value && vscode.workspace.getConfiguration("extensions", null).get<boolean>("autoUpdate")) {
installPrerelease = true;
migratedInsiders.Value = true;
}
} else {
// Reset persistent value, so we register again if they switch to "Insiders" again.
if (migratedInsiders.Value) {
migratedInsiders.Value = false;
}
}

// Mitigate an issue with VS Code not recognizing a programmatically installed VSIX as Prerelease.
// If using VS Code Insiders, and updateChannel is not explicitly set, default to Prerelease.
// Only do this once. If the user manually switches to Release, we don't want to switch them back to Prerelease again.
if (util.isVsCodeInsiders()) {
const insidersMitigationDone: PersistentState<boolean> = new PersistentState<boolean>("CPP.insidersMitigationDone", false);
if (!insidersMitigationDone.Value) {
if (vscode.workspace.getConfiguration("extensions", null).get<boolean>("autoUpdate")) {
if (settings.getWithUndefinedDefault<string>("updateChannel") === undefined) {
installPrerelease = true;
}
}
insidersMitigationDone.Value = true;
}
}

if (installPrerelease) {
vscode.commands.executeCommand("workbench.extensions.installExtension", "ms-vscode.cpptools", { installPreReleaseVersion: true });
}
}
40 changes: 40 additions & 0 deletions Extension/src/SSH/TargetsView/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved.
* See 'LICENSE' in the project root for license information.
* ------------------------------------------------------------------------------------------ */

import * as vscode from 'vscode';

/**
* Base class of nodes in all tree nodes
*/
export interface BaseNode {
/**
* Get the child nodes of this node
*/
getChildren(): Promise<BaseNode[]>;

/**
* Get the vscode.TreeItem associated with this node
*/
getTreeItem(): Promise<vscode.TreeItem>;
}

export class LabelLeafNode implements BaseNode {
constructor(private readonly label: string) { /* blank */ }

async getChildren(): Promise<BaseNode[]> {
return [];
}

getTreeItem(): Promise<vscode.TreeItem> {
return Promise.resolve(new vscode.TreeItem(this.getLabel(), vscode.TreeItemCollapsibleState.None));
}

getLabel(): string {
return this.label;
}
}

export const refreshCppSshTargetsView: string = 'C_Cpp.refreshCppSshTargetsView';
export const addSshTarget: string = 'C_Cpp.addSshTarget';
119 changes: 119 additions & 0 deletions Extension/src/SSH/TargetsView/sshTargetsProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved.
* See 'LICENSE' in the project root for license information.
* ------------------------------------------------------------------------------------------ */

import { getSshConfigHostInfos } from '../sshHosts';
import * as vscode from 'vscode';
import { addSshTarget, BaseNode, LabelLeafNode, refreshCppSshTargetsView } from './common';
import { TargetLeafNode, filesWritable, setActiveSshTarget, _activeTarget, workspaceState_activeSshTarget } from './targetNodes';
import { extensionContext, ISshConfigHostInfo } from '../../common';
import * as nls from 'vscode-nls';

nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize: nls.LocalizeFunc = nls.loadMessageBundle();

let _targets: Map<string, ISshConfigHostInfo> = new Map<string, ISshConfigHostInfo>();

export class SshTargetsProvider implements vscode.TreeDataProvider<BaseNode>, vscode.Disposable {
private readonly _onDidChangeTreeData: vscode.EventEmitter<BaseNode | undefined> = new vscode.EventEmitter<BaseNode | undefined>();

public get onDidChangeTreeData(): vscode.Event<BaseNode | undefined> {
return this._onDidChangeTreeData.event;
}

async getChildren(node?: BaseNode): Promise<BaseNode[]> {
if (node) {
return node.getChildren();
}

const children: BaseNode[] = await this.getTargets();
if (children.length === 0) {
return [new LabelLeafNode(localize('no.ssh.targets', 'No SSH targets'))];
}

return children;
}

getTreeItem(node: BaseNode): Promise<vscode.TreeItem> {
return node.getTreeItem();
}

refresh(node?: BaseNode): void {
this._onDidChangeTreeData.fire(node);
}

dispose(): void {
this._onDidChangeTreeData.dispose();
}

private async getTargets(): Promise<BaseNode[]> {
filesWritable.clear();
_targets = await getSshConfigHostInfos();
const targetNodes: BaseNode[] = [];
// Currently, the best place to check if a connection is removed is during refresh, since the active target could be removed
// by editing the SSH config file directly. If we see any performance issue in the future, we can move this to removeSshTargetImpl,
// and the file watchers.
let activeTargetRemoved: boolean = true;
const cachedActiveTarget: string | undefined = await getActiveSshTarget(false);
for (const host of Array.from(_targets.keys())) {
const sshConfigHostInfo: ISshConfigHostInfo | undefined = _targets.get(host);
if (sshConfigHostInfo) {
targetNodes.push(new TargetLeafNode(host, sshConfigHostInfo));
if (host === cachedActiveTarget) {
activeTargetRemoved = false;
}
}
}
if (activeTargetRemoved) {
setActiveSshTarget(undefined);
}
return targetNodes;
}
}

export async function initializeSshTargets(): Promise<void> {
_targets = await getSshConfigHostInfos();
let activeTargetRemoved: boolean = true;
const cachedActiveTarget: string | undefined = await getActiveSshTarget(false);
for (const host of Array.from(_targets.keys())) {
if (host === cachedActiveTarget) {
activeTargetRemoved = false;
}
}
if (activeTargetRemoved) {
setActiveSshTarget(undefined);
}
await setActiveSshTarget(extensionContext?.workspaceState.get(workspaceState_activeSshTarget));
}

export async function getActiveSshTarget(selectWhenNotSet: boolean = true): Promise<string | undefined> {
if (_targets.size === 0 && !selectWhenNotSet) {
return undefined;
}
if (!_activeTarget && selectWhenNotSet) {
const name: string | undefined = await selectSshTarget();
if (!name) {
throw Error(localize('active.ssh.target.selection.cancelled', 'Active SSH target selection cancelled.'));
}
await setActiveSshTarget(name);
await vscode.commands.executeCommand(refreshCppSshTargetsView);
}
return _activeTarget;
}

const addNewSshTarget: string = localize('add.new.ssh.target', '{0} Add New SSH Target...', '$(plus)');

export async function selectSshTarget(): Promise<string | undefined> {
const items: string[] = Array.from(_targets.keys());
// Special item for adding SSH target
items.push(addNewSshTarget);
const selection: string | undefined = await vscode.window.showQuickPick(items, { title: localize('select.ssh.target', 'Select an SSH target') });
if (!selection) {
return undefined;
}
if (selection === addNewSshTarget) {
return vscode.commands.executeCommand(addSshTarget);
}
return selection;
}
Loading

0 comments on commit 03b3995

Please sign in to comment.