Skip to content

Commit 03b3995

Browse files
authored
SSH Target Selector (#9760)
* SSH Target Selector
1 parent 466fb3b commit 03b3995

21 files changed

+1742
-612
lines changed

Extension/package.json

Lines changed: 555 additions & 418 deletions
Large diffs are not rendered by default.

Extension/package.nls.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@
2929
"c_cpp.command.BuildAndRunFile.title": "Run C/C++ File",
3030
"c_cpp.command.AddDebugConfiguration.title": "Add Debug Configuration",
3131
"c_cpp.command.GenerateDoxygenComment.title": "Generate Doxygen Comment",
32+
"c_cpp.command.addSshTarget.title": "Add SSH target",
33+
"c_cpp.command.removeSshTarget.title": "Remove SSH target",
34+
"c_cpp.command.setActiveSshTarget.title": "Set this SSH target as the active target",
35+
"c_cpp.command.selectActiveSshTarget.title": "Select an active SSH target",
36+
"c_cpp.command.selectSshTarget.title": "Select SSH target",
37+
"c_cpp.command.activeSshTarget.title": "Get the active SSH target",
38+
"c_cpp.command.refreshCppSshTargetsView.title": "Refresh",
3239
"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." ] },
3340
"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." ] },
3441
"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." ] },
@@ -232,6 +239,7 @@
232239
"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.",
233240
"c_cpp.configuration.legacyCompilerArgsBehavior.deprecationMessage": "This setting is temporary to support transitioning to corrected behavior in v1.10.0.",
234241
"c_cpp.contributes.views.cppReferencesView.title": "C/C++: Other references results",
242+
"c_cpp.contributes.views.sshTargetsView.title": { "message": "Cpptools: SSH targets", "comment": [ "Do not localize `Cpptools`." ] },
235243
"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" ] },
236244
"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.",
237245
"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).",

Extension/src/Debugger/configurationProvider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,7 +1030,7 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
10301030
logger.getOutputChannelLogger().showErrorMessage(localize('missing.properties.copyFile', '"host", "files", and "targetDir" are required in {0} steps.', isScp ? 'SCP' : 'rsync'));
10311031
return false;
10321032
}
1033-
const host: util.ISshHostInfo = { hostName: step.host.hostName, user: step.host.user, port: step.host.port };
1033+
const host: util.ISshHostInfo = util.isString(step.host) ? { hostName: step.host } : { hostName: step.host.hostName, user: step.host.user, port: step.host.port };
10341034
const jumpHosts: util.ISshHostInfo[] = step.host.jumpHosts;
10351035
let files: vscode.Uri[] = [];
10361036
if (util.isString(step.files)) {
@@ -1061,7 +1061,7 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
10611061
logger.getOutputChannelLogger().showErrorMessage(localize('missing.properties.ssh', '"host" and "command" are required for ssh steps.'));
10621062
return false;
10631063
}
1064-
const host: util.ISshHostInfo = { hostName: step.host.hostName, user: step.host.user, port: step.host.port };
1064+
const host: util.ISshHostInfo = util.isString(step.host) ? { hostName: step.host } : { hostName: step.host.hostName, user: step.host.user, port: step.host.port };
10651065
const jumpHosts: util.ISshHostInfo[] = step.host.jumpHosts;
10661066
const localForwards: util.ISshLocalForwardInfo[] = step.host.localForwards;
10671067
const continueOn: string = step.continueOn;

Extension/src/Debugger/extension.ts

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,22 @@ import { DebugConfigurationProvider, ConfigurationAssetProviderFactory, Configur
1111
import { CppdbgDebugAdapterDescriptorFactory, CppvsdbgDebugAdapterDescriptorFactory } from './debugAdapterDescriptorFactory';
1212
import { DebuggerType } from './configurations';
1313
import * as nls from 'vscode-nls';
14+
import { getActiveSshTarget, initializeSshTargets, selectSshTarget, SshTargetsProvider } from '../SSH/TargetsView/sshTargetsProvider';
15+
import { addSshTarget, BaseNode, refreshCppSshTargetsView } from '../SSH/TargetsView/common';
16+
import { setActiveSshTarget, TargetLeafNode } from '../SSH/TargetsView/targetNodes';
17+
import { sshCommandToConfig } from '../SSH/sshCommandToConfig';
18+
import { getSshConfiguration, getSshConfigurationFiles, writeSshConfiguration } from '../SSH/sshHosts';
19+
import { pathAccessible } from '../common';
20+
import * as fs from 'fs';
21+
import { Configuration } from 'ssh-config';
22+
import * as chokidar from 'chokidar';
1423

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

28+
const fileWatchers: chokidar.FSWatcher[] = [];
29+
1930
export async function initialize(context: vscode.ExtensionContext): Promise<void> {
2031
// Activate Process Picker Commands
2132
const attachItemsProvider: AttachItemsProvider = NativeAttachItemsProviderFactory.Get();
@@ -64,9 +75,87 @@ export async function initialize(context: vscode.ExtensionContext): Promise<void
6475
disposables.push(vscode.debug.registerDebugAdapterDescriptorFactory(DebuggerType.cppvsdbg , new CppvsdbgDebugAdapterDescriptorFactory(context)));
6576
disposables.push(vscode.debug.registerDebugAdapterDescriptorFactory(DebuggerType.cppdbg, new CppdbgDebugAdapterDescriptorFactory(context)));
6677

67-
vscode.Disposable.from(...disposables);
78+
// SSH Targets View
79+
await initializeSshTargets();
80+
const sshTargetsProvider: SshTargetsProvider = new SshTargetsProvider();
81+
disposables.push(vscode.window.registerTreeDataProvider('CppSshTargetsView', sshTargetsProvider));
82+
disposables.push(vscode.commands.registerCommand(addSshTarget, addSshTargetImpl));
83+
disposables.push(vscode.commands.registerCommand('C_Cpp.removeSshTarget', removeSshTargetImpl));
84+
disposables.push(vscode.commands.registerCommand(refreshCppSshTargetsView, (node?: BaseNode) => sshTargetsProvider.refresh(node)));
85+
disposables.push(vscode.commands.registerCommand('C_Cpp.setActiveSshTarget', async (node: TargetLeafNode) => {
86+
await setActiveSshTarget(node.name);
87+
await vscode.commands.executeCommand(refreshCppSshTargetsView);
88+
}));
89+
disposables.push(vscode.commands.registerCommand('C_Cpp.selectSshTarget', selectSshTarget));
90+
disposables.push(vscode.commands.registerCommand('C_Cpp.selectActiveSshTarget', async () => {
91+
const name: string | undefined = await selectSshTarget();
92+
if (name) {
93+
await setActiveSshTarget(name);
94+
await vscode.commands.executeCommand(refreshCppSshTargetsView);
95+
}
96+
}));
97+
disposables.push(vscode.commands.registerCommand('C_Cpp.activeSshTarget', getActiveSshTarget));
98+
disposables.push(sshTargetsProvider);
99+
for (const sshConfig of getSshConfigurationFiles()) {
100+
fileWatchers.push(chokidar.watch(sshConfig, {ignoreInitial: true})
101+
.on('add', () => vscode.commands.executeCommand(refreshCppSshTargetsView))
102+
.on('change', () => vscode.commands.executeCommand(refreshCppSshTargetsView))
103+
.on('unlink', () => vscode.commands.executeCommand(refreshCppSshTargetsView)));
104+
}
105+
vscode.commands.executeCommand('setContext', 'showCppSshTargetsView', true);
68106
}
69107

70108
export function dispose(): void {
109+
// Don't wait
110+
fileWatchers.forEach(watcher => watcher.close().then(() => {}, () => {}));
71111
disposables.forEach(d => d.dispose());
72112
}
113+
114+
async function addSshTargetImpl(): Promise<string> {
115+
const name: string | undefined = await vscode.window.showInputBox({
116+
title: localize('enter.ssh.target.name', 'Enter SSH Target Name'),
117+
placeHolder: localize('ssh.target.name.place.holder', 'Example: `mySSHTarget`'),
118+
ignoreFocusOut: true
119+
});
120+
if (name === undefined) {
121+
// Cancelled
122+
return '';
123+
}
124+
125+
const command: string | undefined = await vscode.window.showInputBox({
126+
title: localize('enter.ssh.connection.command', 'Enter SSH Connection Command'),
127+
placeHolder: localize('ssh.connection.command.place.holder', 'Example: `ssh [email protected] -A`'),
128+
ignoreFocusOut: true
129+
});
130+
if (!command) {
131+
return '';
132+
}
133+
134+
const newEntry: { [key: string]: string } = sshCommandToConfig(command, name);
135+
136+
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') });
137+
if (!targetFile) {
138+
return '';
139+
}
140+
141+
const parsedSshConfig: Configuration = await getSshConfiguration(targetFile, false);
142+
parsedSshConfig.prepend(newEntry, true);
143+
await writeSshConfiguration(targetFile, parsedSshConfig);
144+
145+
return name;
146+
}
147+
148+
async function removeSshTargetImpl(node: TargetLeafNode): Promise<boolean> {
149+
const labelYes: string = localize('yes', 'Yes');
150+
const labelNo: string = localize('no', 'No');
151+
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);
152+
if (!confirm || confirm === labelNo) {
153+
return false;
154+
}
155+
156+
const parsedSshConfig: Configuration = await getSshConfiguration(node.sshConfigHostInfo.file, false);
157+
parsedSshConfig.remove({ Host: node.name });
158+
await writeSshConfiguration(node.sshConfigHostInfo.file, parsedSshConfig);
159+
160+
return true;
161+
}

Extension/src/LanguageServer/extension.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import * as yauzl from 'yauzl';
2626
import { Readable } from 'stream';
2727
import * as nls from 'vscode-nls';
2828
import { CppBuildTaskProvider } from './cppBuildTaskProvider';
29-
import { UpdateInsidersAccess } from '../main';
3029

3130
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
3231
const localize: nls.LocalizeFunc = nls.loadMessageBundle();
@@ -1023,3 +1022,42 @@ export function getClients(): ClientCollection {
10231022
export function getActiveClient(): Client {
10241023
return clients.ActiveClient;
10251024
}
1025+
1026+
export function UpdateInsidersAccess(): void {
1027+
let installPrerelease: boolean = false;
1028+
1029+
// Only move them to the new prerelease mechanism if using updateChannel of Insiders.
1030+
const settings: CppSettings = new CppSettings();
1031+
const migratedInsiders: PersistentState<boolean> = new PersistentState<boolean>("CPP.migratedInsiders", false);
1032+
if (settings.updateChannel === "Insiders") {
1033+
// Don't do anything while the user has autoUpdate disabled, so we do not cause the extension to be updated.
1034+
if (!migratedInsiders.Value && vscode.workspace.getConfiguration("extensions", null).get<boolean>("autoUpdate")) {
1035+
installPrerelease = true;
1036+
migratedInsiders.Value = true;
1037+
}
1038+
} else {
1039+
// Reset persistent value, so we register again if they switch to "Insiders" again.
1040+
if (migratedInsiders.Value) {
1041+
migratedInsiders.Value = false;
1042+
}
1043+
}
1044+
1045+
// Mitigate an issue with VS Code not recognizing a programmatically installed VSIX as Prerelease.
1046+
// If using VS Code Insiders, and updateChannel is not explicitly set, default to Prerelease.
1047+
// Only do this once. If the user manually switches to Release, we don't want to switch them back to Prerelease again.
1048+
if (util.isVsCodeInsiders()) {
1049+
const insidersMitigationDone: PersistentState<boolean> = new PersistentState<boolean>("CPP.insidersMitigationDone", false);
1050+
if (!insidersMitigationDone.Value) {
1051+
if (vscode.workspace.getConfiguration("extensions", null).get<boolean>("autoUpdate")) {
1052+
if (settings.getWithUndefinedDefault<string>("updateChannel") === undefined) {
1053+
installPrerelease = true;
1054+
}
1055+
}
1056+
insidersMitigationDone.Value = true;
1057+
}
1058+
}
1059+
1060+
if (installPrerelease) {
1061+
vscode.commands.executeCommand("workbench.extensions.installExtension", "ms-vscode.cpptools", { installPreReleaseVersion: true });
1062+
}
1063+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/* --------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All Rights Reserved.
3+
* See 'LICENSE' in the project root for license information.
4+
* ------------------------------------------------------------------------------------------ */
5+
6+
import * as vscode from 'vscode';
7+
8+
/**
9+
* Base class of nodes in all tree nodes
10+
*/
11+
export interface BaseNode {
12+
/**
13+
* Get the child nodes of this node
14+
*/
15+
getChildren(): Promise<BaseNode[]>;
16+
17+
/**
18+
* Get the vscode.TreeItem associated with this node
19+
*/
20+
getTreeItem(): Promise<vscode.TreeItem>;
21+
}
22+
23+
export class LabelLeafNode implements BaseNode {
24+
constructor(private readonly label: string) { /* blank */ }
25+
26+
async getChildren(): Promise<BaseNode[]> {
27+
return [];
28+
}
29+
30+
getTreeItem(): Promise<vscode.TreeItem> {
31+
return Promise.resolve(new vscode.TreeItem(this.getLabel(), vscode.TreeItemCollapsibleState.None));
32+
}
33+
34+
getLabel(): string {
35+
return this.label;
36+
}
37+
}
38+
39+
export const refreshCppSshTargetsView: string = 'C_Cpp.refreshCppSshTargetsView';
40+
export const addSshTarget: string = 'C_Cpp.addSshTarget';
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/* --------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All Rights Reserved.
3+
* See 'LICENSE' in the project root for license information.
4+
* ------------------------------------------------------------------------------------------ */
5+
6+
import { getSshConfigHostInfos } from '../sshHosts';
7+
import * as vscode from 'vscode';
8+
import { addSshTarget, BaseNode, LabelLeafNode, refreshCppSshTargetsView } from './common';
9+
import { TargetLeafNode, filesWritable, setActiveSshTarget, _activeTarget, workspaceState_activeSshTarget } from './targetNodes';
10+
import { extensionContext, ISshConfigHostInfo } from '../../common';
11+
import * as nls from 'vscode-nls';
12+
13+
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
14+
const localize: nls.LocalizeFunc = nls.loadMessageBundle();
15+
16+
let _targets: Map<string, ISshConfigHostInfo> = new Map<string, ISshConfigHostInfo>();
17+
18+
export class SshTargetsProvider implements vscode.TreeDataProvider<BaseNode>, vscode.Disposable {
19+
private readonly _onDidChangeTreeData: vscode.EventEmitter<BaseNode | undefined> = new vscode.EventEmitter<BaseNode | undefined>();
20+
21+
public get onDidChangeTreeData(): vscode.Event<BaseNode | undefined> {
22+
return this._onDidChangeTreeData.event;
23+
}
24+
25+
async getChildren(node?: BaseNode): Promise<BaseNode[]> {
26+
if (node) {
27+
return node.getChildren();
28+
}
29+
30+
const children: BaseNode[] = await this.getTargets();
31+
if (children.length === 0) {
32+
return [new LabelLeafNode(localize('no.ssh.targets', 'No SSH targets'))];
33+
}
34+
35+
return children;
36+
}
37+
38+
getTreeItem(node: BaseNode): Promise<vscode.TreeItem> {
39+
return node.getTreeItem();
40+
}
41+
42+
refresh(node?: BaseNode): void {
43+
this._onDidChangeTreeData.fire(node);
44+
}
45+
46+
dispose(): void {
47+
this._onDidChangeTreeData.dispose();
48+
}
49+
50+
private async getTargets(): Promise<BaseNode[]> {
51+
filesWritable.clear();
52+
_targets = await getSshConfigHostInfos();
53+
const targetNodes: BaseNode[] = [];
54+
// Currently, the best place to check if a connection is removed is during refresh, since the active target could be removed
55+
// by editing the SSH config file directly. If we see any performance issue in the future, we can move this to removeSshTargetImpl,
56+
// and the file watchers.
57+
let activeTargetRemoved: boolean = true;
58+
const cachedActiveTarget: string | undefined = await getActiveSshTarget(false);
59+
for (const host of Array.from(_targets.keys())) {
60+
const sshConfigHostInfo: ISshConfigHostInfo | undefined = _targets.get(host);
61+
if (sshConfigHostInfo) {
62+
targetNodes.push(new TargetLeafNode(host, sshConfigHostInfo));
63+
if (host === cachedActiveTarget) {
64+
activeTargetRemoved = false;
65+
}
66+
}
67+
}
68+
if (activeTargetRemoved) {
69+
setActiveSshTarget(undefined);
70+
}
71+
return targetNodes;
72+
}
73+
}
74+
75+
export async function initializeSshTargets(): Promise<void> {
76+
_targets = await getSshConfigHostInfos();
77+
let activeTargetRemoved: boolean = true;
78+
const cachedActiveTarget: string | undefined = await getActiveSshTarget(false);
79+
for (const host of Array.from(_targets.keys())) {
80+
if (host === cachedActiveTarget) {
81+
activeTargetRemoved = false;
82+
}
83+
}
84+
if (activeTargetRemoved) {
85+
setActiveSshTarget(undefined);
86+
}
87+
await setActiveSshTarget(extensionContext?.workspaceState.get(workspaceState_activeSshTarget));
88+
}
89+
90+
export async function getActiveSshTarget(selectWhenNotSet: boolean = true): Promise<string | undefined> {
91+
if (_targets.size === 0 && !selectWhenNotSet) {
92+
return undefined;
93+
}
94+
if (!_activeTarget && selectWhenNotSet) {
95+
const name: string | undefined = await selectSshTarget();
96+
if (!name) {
97+
throw Error(localize('active.ssh.target.selection.cancelled', 'Active SSH target selection cancelled.'));
98+
}
99+
await setActiveSshTarget(name);
100+
await vscode.commands.executeCommand(refreshCppSshTargetsView);
101+
}
102+
return _activeTarget;
103+
}
104+
105+
const addNewSshTarget: string = localize('add.new.ssh.target', '{0} Add New SSH Target...', '$(plus)');
106+
107+
export async function selectSshTarget(): Promise<string | undefined> {
108+
const items: string[] = Array.from(_targets.keys());
109+
// Special item for adding SSH target
110+
items.push(addNewSshTarget);
111+
const selection: string | undefined = await vscode.window.showQuickPick(items, { title: localize('select.ssh.target', 'Select an SSH target') });
112+
if (!selection) {
113+
return undefined;
114+
}
115+
if (selection === addNewSshTarget) {
116+
return vscode.commands.executeCommand(addSshTarget);
117+
}
118+
return selection;
119+
}

0 commit comments

Comments
 (0)