Skip to content

Commit 16b4710

Browse files
authored
Merge pull request #9561 from microsoft/seanmcm/revert2DeploySteps
Revert deploy steps
2 parents 08776b7 + 3d58b64 commit 16b4710

17 files changed

+2683
-82
lines changed

Extension/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ localized_string_ids.h
3333
# ignore generate files. It will be generated when building
3434
src/nativeStrings.ts
3535

36+
vscode*.d.ts

Extension/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# C/C++ for Visual Studio Code Change Log
22

3+
## Version 1.11.2 (pre-release): July 8, 2022
4+
### Enhancements
5+
* Add deploySteps and variables to cppdbg. [PR #9418](https://github.com/microsoft/vscode-cpptools/pull/9418)
6+
37
## Version 1.11.1 (pre-release): July 7, 2022
48
### Enhancements
59
* Move "auto" inlay hints to the right by default and add `C_Cpp.inlayHints.autoDeclarationTypes.showOnLeft`. [#9478](https://github.com/microsoft/vscode-cpptools/issues/9478)

Extension/package.json

Lines changed: 877 additions & 28 deletions
Large diffs are not rendered by default.

Extension/package.nls.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,35 @@
301301
"c_cpp.debuggers.hardwareBreakpoints.description": "Explicit control of hardware breakpoint behavior for remote targets.",
302302
"c_cpp.debuggers.hardwareBreakpoints.require.description": "If true, always use hardware breakpoints. Defaults to false.",
303303
"c_cpp.debuggers.hardwareBreakpoints.limit.description": "Optional limit on the number of available hardware breakpoints to use. Only enforced when \"require\" is true and \"limit\" is greater than 0. Defaults to 0.",
304+
"c_cpp.debuggers.variables.description": "Variables for recursive substitution in this launch configuration. Each variable may refer to others.",
305+
"c_cpp.debuggers.variables.properties.description": "Variable for recursive substitution in this launch configuration. The value may refer to other variables.",
306+
"c_cpp.debuggers.host.description": "Host information.",
307+
"c_cpp.debuggers.host.user.description": "User logging into the host.",
308+
"c_cpp.debuggers.host.hostName.description": "Host name.",
309+
"c_cpp.debuggers.host.port.description": "SSH port on the host. Default is 22.",
310+
"c_cpp.debuggers.host.jumpHost.description": "Connect to the target host by first making a connection to the jump hosts.",
311+
"c_cpp.debuggers.host.localForward.description": "Forward connections to the given TCP port or Unix socket on the local (client) host to the given host and port, or Unix socket, on the remote side",
312+
"c_cpp.debuggers.host.localForward.bindAddress.description": "Local address",
313+
"c_cpp.debuggers.host.localForward.port.description": "Local port",
314+
"c_cpp.debuggers.host.localForward.host.description": "Host name",
315+
"c_cpp.debuggers.host.localForward.hostPort.description": "Host port",
316+
"c_cpp.debuggers.host.localForward.localSocket.description": "Local socket",
317+
"c_cpp.debuggers.host.localForward.remoteSocket.description": "Remote socket",
318+
"c_cpp.debuggers.deploySteps.description": "Steps needed to deploy the application. Order matters.",
319+
"c_cpp.debuggers.deploySteps.scp.description": "Copy files using SCP.",
320+
"c_cpp.debuggers.deploySteps.scp.files.description": "Files to be copied via SCP. Supports path pattern.",
321+
"c_cpp.debuggers.deploySteps.scp.targetDir.description": "Target directory.",
322+
"c_cpp.debuggers.deploySteps.scp.scpPath.description": "Optional full path to SCP. Assumes SCP is on PATH if not specified",
323+
"c_cpp.debuggers.deploySteps.debug": "If true, skip when starting without debugging. If false, skip when starting debugging. If undefined, never skip.",
324+
"c_cpp.debuggers.deploySteps.ssh.description": "SSH command step.",
325+
"c_cpp.debuggers.deploySteps.ssh.command.description": "Command to be executed via SSH. The command after '-c' in SSH command.",
326+
"c_cpp.debuggers.deploySteps.ssh.sshPath.description": "Optional full path to SSH. Assumes SSH is on PATH if not specified",
327+
"c_cpp.debuggers.deploySteps.continueOn.description": "An optional finish pattern in output. When this pattern is seen in the output, continue the deploy procedures regardless of whether this step returns.",
328+
"c_cpp.debuggers.deploySteps.shell.description": "Shell command step.",
329+
"c_cpp.debuggers.deploySteps.shell.command.description": "Shell command to be executed.",
330+
"c_cpp.debuggers.vsCodeCommand.description": "VS Code command to be invoked. Can be a command in VS Code or an active extension.",
331+
"c_cpp.debuggers.vsCodeCommand.command.description": "VS Code command to be invoked.",
332+
"c_cpp.debuggers.vsCodeCommand.args.description": "Arguments to the VS Code command.",
304333
"c_cpp.taskDefinitions.name.description": "The name of the task.",
305334
"c_cpp.taskDefinitions.command.description": "The path to either a compiler or script that performs compilation.",
306335
"c_cpp.taskDefinitions.args.description": "Additional arguments to pass to the compiler or compilation script.",

Extension/src/Debugger/configurationProvider.ts

Lines changed: 148 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,24 @@ import { PlatformInformation } from '../platform';
2323
import { Environment, ParsedEnvironmentFile } from './ParsedEnvironmentFile';
2424
import { CppSettings, OtherSettings } from '../LanguageServer/settings';
2525
import { configPrefix } from '../LanguageServer/extension';
26+
import { expandAllStrings, ExpansionOptions, ExpansionVars } from '../expand';
27+
import { scp, ssh } from '../SSH/commands';
28+
import * as glob from 'glob';
29+
import { promisify } from 'util';
2630

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

34+
enum StepType {
35+
scp = 'scp',
36+
ssh = 'ssh',
37+
shell = 'shell',
38+
remoteShell = 'remoteShell',
39+
command = 'command'
40+
}
41+
42+
const globAsync: (pattern: string, options?: glob.IOptions | undefined) => Promise<string[]> = promisify(glob);
43+
3044
/*
3145
* Retrieves configurations from a provider and displays them in a quickpick menu to be selected.
3246
* Ensures that the selected configuration's preLaunchTask (if existent) is populated in the user's task.json.
@@ -205,11 +219,11 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
205219
* This hook is directly called after 'resolveDebugConfiguration' but with all variables substituted.
206220
* This is also ran after the tasks.json has completed.
207221
*
208-
* Try to add all missing attributes to the debug configuration being launched.
222+
* Try to add all missing attributes to the debug configuration being launched.
209223
* If return "undefined", the debugging will be aborted silently.
210224
* If return "null", the debugging will be aborted and launch.json will be opened.
211-
*/
212-
resolveDebugConfigurationWithSubstitutedVariables(folder: vscode.WorkspaceFolder | undefined, config: CppDebugConfiguration, token?: vscode.CancellationToken): vscode.ProviderResult<CppDebugConfiguration> {
225+
*/
226+
async resolveDebugConfigurationWithSubstitutedVariables(folder: vscode.WorkspaceFolder | undefined, config: CppDebugConfiguration, token?: vscode.CancellationToken): Promise<CppDebugConfiguration | null | undefined> {
213227
if (!config || !config.type) {
214228
return undefined; // Abort debugging silently.
215229
}
@@ -232,7 +246,7 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
232246

233247
// Disable debug heap by default, enable if 'enableDebugHeap' is set.
234248
if (!config.enableDebugHeap) {
235-
const disableDebugHeapEnvSetting: Environment = {"name" : "_NO_DEBUG_HEAP", "value" : "1"};
249+
const disableDebugHeapEnvSetting: Environment = { "name": "_NO_DEBUG_HEAP", "value": "1" };
236250

237251
if (config.environment && util.isArray(config.environment)) {
238252
config.environment.push(disableDebugHeapEnvSetting);
@@ -245,6 +259,8 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
245259
// Add environment variables from .env file
246260
this.resolveEnvFile(config, folder);
247261

262+
await this.expand(config, folder);
263+
248264
this.resolveSourceFileMapVariables(config);
249265

250266
// Modify WSL config for OpenDebugAD7
@@ -307,6 +323,19 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
307323
// logger.showOutputChannel();
308324
}
309325

326+
// Run deploy steps
327+
if (config.deploySteps && config.deploySteps.length !== 0) {
328+
const codeVersion: number[] = vscode.version.split('.').map(num => parseInt(num, undefined));
329+
if ((util.isNumber(codeVersion[0]) && codeVersion[0] < 1) || (util.isNumber(codeVersion[0]) && codeVersion[0] === 1 && util.isNumber(codeVersion[1]) && codeVersion[1] < 69)) {
330+
logger.getOutputChannelLogger().showErrorMessage(localize("vs.code.1.69+.required", "'deploySteps' require VS Code 1.69+."));
331+
return undefined;
332+
}
333+
const deploySucceeded: boolean = await this.deploySteps(config, token);
334+
if (!deploySucceeded || token?.isCancellationRequested) {
335+
return undefined;
336+
}
337+
}
338+
310339
return config;
311340
}
312341

@@ -595,17 +624,17 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
595624
const newSourceFileMapTarget: string = util.resolveVariables(sourceFileMapTarget, undefined);
596625
if (sourceFileMapTarget !== newSourceFileMapTarget) {
597626
// Add a space if source was changed, else just tab the target message.
598-
message += (message ? ' ' : '\t');
627+
message += (message ? ' ' : '\t');
599628
message += localize("replacing.targetpath", "Replacing {0} '{1}' with '{2}'.", "targetPath", sourceFileMapTarget, newSourceFileMapTarget);
600629
target = newSourceFileMapTarget;
601630
}
602631
} else if (util.isObject(sourceFileMapTarget)) {
603-
const newSourceFileMapTarget: {"editorPath": string; "useForBreakpoints": boolean } = sourceFileMapTarget;
632+
const newSourceFileMapTarget: { "editorPath": string; "useForBreakpoints": boolean } = sourceFileMapTarget;
604633
newSourceFileMapTarget["editorPath"] = util.resolveVariables(sourceFileMapTarget["editorPath"], undefined);
605634

606635
if (sourceFileMapTarget !== newSourceFileMapTarget) {
607636
// Add a space if source was changed, else just tab the target message.
608-
message += (message ? ' ' : '\t');
637+
message += (message ? ' ' : '\t');
609638
message += localize("replacing.editorPath", "Replacing {0} '{1}' with '{2}'.", "editorPath", sourceFileMapTarget, newSourceFileMapTarget["editorPath"]);
610639
target = newSourceFileMapTarget;
611640
}
@@ -844,7 +873,7 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
844873
}
845874
selectedConfig.debugType = debugModeOn ? DebugType.debug : DebugType.run;
846875
// startDebugging will trigger a call to resolveDebugConfiguration.
847-
await vscode.debug.startDebugging(folder, selectedConfig, {noDebug: !debugModeOn});
876+
await vscode.debug.startDebugging(folder, selectedConfig, { noDebug: !debugModeOn });
848877
}
849878

850879
private async selectConfiguration(textEditor: vscode.TextEditor, pickDefault: boolean = true, onlyWorkspaceFolder: boolean = false): Promise<CppDebugConfiguration | undefined> {
@@ -912,6 +941,116 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
912941
}
913942
}
914943
}
944+
945+
private async expand(config: vscode.DebugConfiguration, folder: vscode.WorkspaceFolder | undefined): Promise<void> {
946+
const folderPath: string | undefined = folder?.uri.fsPath || vscode.workspace.workspaceFolders?.[0].uri.fsPath;
947+
const vars: ExpansionVars = config.variables ? config.variables : {};
948+
vars.workspaceFolder = folderPath || '{workspaceFolder}';
949+
vars.workspaceFolderBasename = folderPath ? path.basename(folderPath) : '{workspaceFolderBasename}';
950+
const expansionOptions: ExpansionOptions = { vars, recursive: true };
951+
return expandAllStrings(config, expansionOptions);
952+
}
953+
954+
// Returns true when ALL steps succeed; stop all subsequent steps if one fails
955+
private async deploySteps(config: vscode.DebugConfiguration, cancellationToken?: vscode.CancellationToken): Promise<boolean> {
956+
let succeeded: boolean = true;
957+
const deployStart: number = new Date().getTime();
958+
959+
for (const step of config.deploySteps) {
960+
succeeded = await this.singleDeployStep(config, step, cancellationToken);
961+
if (!succeeded) {
962+
break;
963+
}
964+
}
965+
966+
const deployEnd: number = new Date().getTime();
967+
968+
const telemetryProperties: { [key: string]: string } = {
969+
Succeeded: `${succeeded}`,
970+
IsDebugging: `${!config.noDebug || false}`
971+
};
972+
const telemetryMetrics: { [key: string]: number } = {
973+
NumSteps: config.deploySteps.length,
974+
Duration: deployEnd - deployStart
975+
};
976+
Telemetry.logDebuggerEvent('deploy', telemetryProperties, telemetryMetrics);
977+
978+
return succeeded;
979+
}
980+
981+
private async singleDeployStep(config: vscode.DebugConfiguration, step: any, cancellationToken?: vscode.CancellationToken): Promise<boolean> {
982+
if ((config.noDebug && step.debug === true) || (!config.noDebug && step.debug === false)) {
983+
// Skip steps that doesn't match current launch mode. Explicit true/false check, since a step is always run when debug is undefined.
984+
return true;
985+
}
986+
switch (step.type) {
987+
case StepType.command: {
988+
// VS Code commands are the same regardless of which extension invokes them, so just invoke them here.
989+
if (step.args && !Array.isArray(step.args)) {
990+
logger.getOutputChannelLogger().showErrorMessage(localize('command.args.must.be.array', '"args" in command deploy step must be an array.'));
991+
return false;
992+
}
993+
const returnCode: unknown = await vscode.commands.executeCommand(step.command, ...step.args);
994+
return !returnCode;
995+
}
996+
case StepType.scp: {
997+
if (!step.files || !step.targetDir || !step.host) {
998+
logger.getOutputChannelLogger().showErrorMessage(localize('missing.properties.scp', '"host", "files", and "targetDir" are required in scp steps.'));
999+
return false;
1000+
}
1001+
const host: util.ISshHostInfo = { hostName: step.host.hostName, user: step.host.user, port: step.host.port };
1002+
const jumpHosts: util.ISshHostInfo[] = step.host.jumpHosts;
1003+
let files: vscode.Uri[] = [];
1004+
if (util.isString(step.files)) {
1005+
files = files.concat((await globAsync(step.files)).map(file => vscode.Uri.file(file)));
1006+
} else if (util.isArrayOfString(step.files)) {
1007+
for (const fileGlob of (step.files as string[])) {
1008+
files = files.concat((await globAsync(fileGlob)).map(file => vscode.Uri.file(file)));
1009+
}
1010+
} else {
1011+
logger.getOutputChannelLogger().showErrorMessage(localize('incorrect.files.type.scp', '"files" must be a string or an array of strings in scp steps.'));
1012+
return false;
1013+
}
1014+
const scpResult: util.ProcessReturnType = await scp(files, host, step.targetDir, config.scpPath, jumpHosts, cancellationToken);
1015+
if (!scpResult.succeeded || cancellationToken?.isCancellationRequested) {
1016+
return false;
1017+
}
1018+
break;
1019+
}
1020+
case StepType.ssh: {
1021+
if (!step.host || !step.command) {
1022+
logger.getOutputChannelLogger().showErrorMessage(localize('missing.properties.ssh', '"host" and "command" are required for ssh steps.'));
1023+
return false;
1024+
}
1025+
const host: util.ISshHostInfo = { hostName: step.host.hostName, user: step.host.user, port: step.host.port };
1026+
const jumpHosts: util.ISshHostInfo[] = step.host.jumpHosts;
1027+
const localForwards: util.ISshLocalForwardInfo[] = step.host.localForwards;
1028+
const continueOn: string = step.continueOn;
1029+
const sshResult: util.ProcessReturnType = await ssh(host, step.command, config.sshPath, jumpHosts, localForwards, continueOn, cancellationToken);
1030+
if (!sshResult.succeeded || cancellationToken?.isCancellationRequested) {
1031+
return false;
1032+
}
1033+
break;
1034+
}
1035+
case StepType.shell: {
1036+
if (!step.command) {
1037+
logger.getOutputChannelLogger().showErrorMessage(localize('missing.properties.shell', '"command" is required for shell steps.'));
1038+
return false;
1039+
}
1040+
const taskResult: util.ProcessReturnType = await util.spawnChildProcess(step.command, undefined, step.continueOn);
1041+
if (!taskResult.succeeded || cancellationToken?.isCancellationRequested) {
1042+
logger.getOutputChannelLogger().showErrorMessage(taskResult.output);
1043+
return false;
1044+
}
1045+
break;
1046+
}
1047+
default: {
1048+
logger.getOutputChannelLogger().appendLine(localize('deploy.step.type.not.supported', 'Deploy step type {0} is not supported.', step.type));
1049+
return false;
1050+
}
1051+
}
1052+
return true;
1053+
}
9151054
}
9161055

9171056
export interface IConfigurationAssetProvider {
@@ -1064,7 +1203,7 @@ export class ConfigurationSnippetProvider implements vscode.CompletionItemProvid
10641203
items = [];
10651204

10661205
// Make a copy of each snippet since we are adding a comma to the end of the insertText.
1067-
this.snippets.forEach((item) => items.push({...item}));
1206+
this.snippets.forEach((item) => items.push({ ...item }));
10681207

10691208
items.map((item) => {
10701209
item.insertText = item.insertText + ','; // Add comma

0 commit comments

Comments
 (0)