Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSH Target Selector #9760

Merged
merged 15 commits into from
Sep 1, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 currently active SSH target",
xisui-MSFT marked this conversation as resolved.
Show resolved Hide resolved
"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`." ] },
xisui-MSFT marked this conversation as resolved.
Show resolved Hide resolved
"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 @@ -1026,7 +1026,7 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
logger.getOutputChannelLogger().showErrorMessage(localize('missing.properties.scp', '"host", "files", and "targetDir" are required in scp 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;
let files: vscode.Uri[] = [];
if (util.isString(step.files)) {
Expand All @@ -1050,7 +1050,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
95 changes: 94 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,91 @@ 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 a SSH configuration file') });
xisui-MSFT marked this conversation as resolved.
Show resolved Hide resolved
if (!targetFile) {
return '';
}

const parsed: Configuration = await getSshConfiguration(targetFile, false);
xisui-MSFT marked this conversation as resolved.
Show resolved Hide resolved
parsed.prepend(newEntry, true);
await writeSshConfiguration(targetFile, parsed);

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;
}

if (await getActiveSshTarget(false) === node.name) {
await setActiveSshTarget(undefined);
}

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

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';
98 changes: 98 additions & 0 deletions Extension/src/SSH/TargetsView/sshTargetsProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/* --------------------------------------------------------------------------------------------
* 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[] = [];
for (const host of Array.from(_targets.keys())) {
const sshConfigHostInfo: ISshConfigHostInfo | undefined = _targets.get(host);
if (sshConfigHostInfo) {
targetNodes.push(new TargetLeafNode(host, sshConfigHostInfo));
}
}
return targetNodes;
}
}

export async function initializeSshTargets(): Promise<void> {
_targets = await getSshConfigHostInfos();
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('no.active.ssh.target', 'No active SSH target.'));
}
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
xisui-MSFT marked this conversation as resolved.
Show resolved Hide resolved
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