@@ -23,10 +23,24 @@ import { PlatformInformation } from '../platform';
2323import { Environment , ParsedEnvironmentFile } from './ParsedEnvironmentFile' ;
2424import { CppSettings , OtherSettings } from '../LanguageServer/settings' ;
2525import { 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
2731nls . config ( { messageFormat : nls . MessageFormat . bundle , bundleFormat : nls . BundleFormat . standalone } ) ( ) ;
2832const 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
9171056export 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