@@ -23,10 +23,24 @@ import { PlatformInformation } from '../platform';
23
23
import { Environment , ParsedEnvironmentFile } from './ParsedEnvironmentFile' ;
24
24
import { CppSettings , OtherSettings } from '../LanguageServer/settings' ;
25
25
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' ;
26
30
27
31
nls . config ( { messageFormat : nls . MessageFormat . bundle , bundleFormat : nls . BundleFormat . standalone } ) ( ) ;
28
32
const localize : nls . LocalizeFunc = nls . loadMessageBundle ( ) ;
29
33
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
+
30
44
/*
31
45
* Retrieves configurations from a provider and displays them in a quickpick menu to be selected.
32
46
* 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
205
219
* This hook is directly called after 'resolveDebugConfiguration' but with all variables substituted.
206
220
* This is also ran after the tasks.json has completed.
207
221
*
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.
209
223
* If return "undefined", the debugging will be aborted silently.
210
224
* 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 > {
213
227
if ( ! config || ! config . type ) {
214
228
return undefined ; // Abort debugging silently.
215
229
}
@@ -232,7 +246,7 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
232
246
233
247
// Disable debug heap by default, enable if 'enableDebugHeap' is set.
234
248
if ( ! config . enableDebugHeap ) {
235
- const disableDebugHeapEnvSetting : Environment = { "name" : "_NO_DEBUG_HEAP" , "value" : "1" } ;
249
+ const disableDebugHeapEnvSetting : Environment = { "name" : "_NO_DEBUG_HEAP" , "value" : "1" } ;
236
250
237
251
if ( config . environment && util . isArray ( config . environment ) ) {
238
252
config . environment . push ( disableDebugHeapEnvSetting ) ;
@@ -245,6 +259,8 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
245
259
// Add environment variables from .env file
246
260
this . resolveEnvFile ( config , folder ) ;
247
261
262
+ await this . expand ( config , folder ) ;
263
+
248
264
this . resolveSourceFileMapVariables ( config ) ;
249
265
250
266
// Modify WSL config for OpenDebugAD7
@@ -307,6 +323,19 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
307
323
// logger.showOutputChannel();
308
324
}
309
325
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
+
310
339
return config ;
311
340
}
312
341
@@ -595,17 +624,17 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
595
624
const newSourceFileMapTarget : string = util . resolveVariables ( sourceFileMapTarget , undefined ) ;
596
625
if ( sourceFileMapTarget !== newSourceFileMapTarget ) {
597
626
// Add a space if source was changed, else just tab the target message.
598
- message += ( message ? ' ' : '\t' ) ;
627
+ message += ( message ? ' ' : '\t' ) ;
599
628
message += localize ( "replacing.targetpath" , "Replacing {0} '{1}' with '{2}'." , "targetPath" , sourceFileMapTarget , newSourceFileMapTarget ) ;
600
629
target = newSourceFileMapTarget ;
601
630
}
602
631
} else if ( util . isObject ( sourceFileMapTarget ) ) {
603
- const newSourceFileMapTarget : { "editorPath" : string ; "useForBreakpoints" : boolean } = sourceFileMapTarget ;
632
+ const newSourceFileMapTarget : { "editorPath" : string ; "useForBreakpoints" : boolean } = sourceFileMapTarget ;
604
633
newSourceFileMapTarget [ "editorPath" ] = util . resolveVariables ( sourceFileMapTarget [ "editorPath" ] , undefined ) ;
605
634
606
635
if ( sourceFileMapTarget !== newSourceFileMapTarget ) {
607
636
// Add a space if source was changed, else just tab the target message.
608
- message += ( message ? ' ' : '\t' ) ;
637
+ message += ( message ? ' ' : '\t' ) ;
609
638
message += localize ( "replacing.editorPath" , "Replacing {0} '{1}' with '{2}'." , "editorPath" , sourceFileMapTarget , newSourceFileMapTarget [ "editorPath" ] ) ;
610
639
target = newSourceFileMapTarget ;
611
640
}
@@ -844,7 +873,7 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
844
873
}
845
874
selectedConfig . debugType = debugModeOn ? DebugType . debug : DebugType . run ;
846
875
// 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 } ) ;
848
877
}
849
878
850
879
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
912
941
}
913
942
}
914
943
}
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
+ }
915
1054
}
916
1055
917
1056
export interface IConfigurationAssetProvider {
@@ -1064,7 +1203,7 @@ export class ConfigurationSnippetProvider implements vscode.CompletionItemProvid
1064
1203
items = [ ] ;
1065
1204
1066
1205
// 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 } ) ) ;
1068
1207
1069
1208
items . map ( ( item ) => {
1070
1209
item . insertText = item . insertText + ',' ; // Add comma
0 commit comments