@@ -17,9 +17,12 @@ interface DevfileTaskDefinition extends vscode.TaskDefinition {
1717 command : string ;
1818 workdir ?: string ;
1919 component ?: string ;
20+ commandId ?: string ;
2021}
2122
2223export class DevfileTaskProvider implements vscode . TaskProvider {
24+ private execTaskById = new Map < string , vscode . Task > ( ) ;
25+ private compositeConfigById = new Map < string , { name : string ; commandIds : string [ ] ; parallel : boolean } > ( ) ;
2326
2427 constructor ( private channel : vscode . OutputChannel , private cheAPI : any , private terminalExtAPI : any ) {
2528 }
@@ -35,15 +38,42 @@ export class DevfileTaskProvider implements vscode.TaskProvider {
3538 private async computeTasks ( ) : Promise < vscode . Task [ ] > {
3639 const devfileCommands = await this . fetchDevfileCommands ( ) ;
3740
38- const cheTasks : vscode . Task [ ] = devfileCommands !
39- . filter ( command => command . exec ?. commandLine )
41+ const localCommands = devfileCommands !
4042 . filter ( command => {
4143 const importedByAttribute = ( command . attributes as any ) ?. [ 'controller.devfile.io/imported-by' ] ;
4244 return ! command . attributes || importedByAttribute === undefined || importedByAttribute === 'parent' ;
4345 } )
44- . filter ( command => ! / ^ i n i t - s s h - a g e n t - c o m m a n d - \d + $ / . test ( command . id ) )
45- . map ( command => this . createCheTask ( command . exec ?. label || command . id , command . exec ?. commandLine ! , command . exec ?. workingDir || '${PROJECT_SOURCE}' , command . exec ?. component ! , command . exec ?. env ) ) ;
46- return cheTasks ;
46+ . filter ( command => ! / ^ i n i t - s s h - a g e n t - c o m m a n d - \d + $ / . test ( command . id ) ) ;
47+
48+ this . execTaskById . clear ( ) ;
49+ const execTasks : vscode . Task [ ] = localCommands
50+ . filter ( command => command . exec ?. commandLine )
51+ . map ( command => {
52+ const task = this . createCheTask (
53+ command . exec ?. label || command . id ,
54+ command . exec ?. commandLine ! ,
55+ command . exec ?. workingDir || '${PROJECT_SOURCE}' ,
56+ command . exec ?. component ! ,
57+ command . exec ?. env ,
58+ command . id
59+ ) ;
60+ this . execTaskById . set ( command . id , task ) ;
61+ return task ;
62+ } ) ;
63+
64+ this . compositeConfigById . clear ( ) ;
65+ const compositeTasks : vscode . Task [ ] = localCommands
66+ . filter ( command => ( command as any ) . composite ?. commands ?. length )
67+ . map ( command => {
68+ const composite = ( command as any ) . composite ;
69+ const name = composite ?. label || command . id ;
70+ const commandIds = composite ?. commands || [ ] ;
71+ const parallel = Boolean ( composite ?. parallel ) ;
72+ this . compositeConfigById . set ( command . id , { name, commandIds, parallel } ) ;
73+ return this . createCompositeTask ( name , commandIds , parallel , command . id ) ;
74+ } ) ;
75+
76+ return [ ...execTasks , ...compositeTasks ] ;
4777 }
4878
4979 private async fetchDevfileCommands ( ) : Promise < V1alpha2DevWorkspaceSpecTemplateCommands [ ] > {
@@ -56,7 +86,14 @@ export class DevfileTaskProvider implements vscode.TaskProvider {
5686 return [ ] ;
5787 }
5888
59- private createCheTask ( name : string , command : string , workdir : string , component : string , env ?: Array < V1alpha2DevWorkspaceSpecTemplateCommandsItemsExecEnv > ) : vscode . Task {
89+ private createCheTask (
90+ name : string ,
91+ command : string ,
92+ workdir : string ,
93+ component : string ,
94+ env ?: Array < V1alpha2DevWorkspaceSpecTemplateCommandsItemsExecEnv > ,
95+ commandId ?: string
96+ ) : vscode . Task {
6097 function expandEnvVariables ( line : string ) : string {
6198 const regex = / \$ { [ a - z A - Z _ ] [ a - z A - Z 0 - 9 _ ] * } / g;
6299 const envArray = line . match ( regex ) ;
@@ -75,7 +112,8 @@ export class DevfileTaskProvider implements vscode.TaskProvider {
75112 type : 'devfile' ,
76113 command,
77114 workdir,
78- component
115+ component,
116+ commandId
79117 } ;
80118
81119 const execution = new vscode . CustomExecution ( async ( ) : Promise < vscode . Pseudoterminal > => {
@@ -92,4 +130,133 @@ export class DevfileTaskProvider implements vscode.TaskProvider {
92130 const task = new vscode . Task ( kind , vscode . TaskScope . Workspace , name , 'devfile' , execution , [ ] ) ;
93131 return task ;
94132 }
133+
134+ private createCompositeTask ( name : string , _commandIds : string [ ] , _parallel : boolean , commandId : string ) : vscode . Task {
135+ const kind : DevfileTaskDefinition = {
136+ type : 'devfile' ,
137+ command : `composite:${ commandId } ` ,
138+ commandId
139+ } ;
140+
141+ const execution = this . createCompositeExecution ( commandId ) ;
142+ const task = new vscode . Task ( kind , vscode . TaskScope . Workspace , name , 'devfile' , execution , [ ] ) ;
143+ task . presentationOptions = {
144+ reveal : vscode . TaskRevealKind . Never ,
145+ panel : vscode . TaskPanelKind . Shared ,
146+ focus : false ,
147+ echo : false ,
148+ showReuseMessage : false ,
149+ close : true
150+ } ;
151+ return task ;
152+ }
153+
154+ private createCompositeExecution ( commandId : string ) : vscode . CustomExecution {
155+ const writeEmitter = new vscode . EventEmitter < string > ( ) ;
156+ const closeEmitter = new vscode . EventEmitter < number | void > ( ) ;
157+ const activeExecutions : vscode . TaskExecution [ ] = [ ] ;
158+
159+ return new vscode . CustomExecution ( async ( ) : Promise < vscode . Pseudoterminal > => {
160+ const pty : vscode . Pseudoterminal = {
161+ onDidWrite : writeEmitter . event ,
162+ onDidClose : closeEmitter . event ,
163+ open : async ( ) => {
164+ let exitCode = 0 ;
165+ try {
166+ const result = await this . runCompositeById ( commandId , activeExecutions ) ;
167+ if ( result . failed ) {
168+ exitCode = 1 ;
169+ }
170+ } catch ( error ) {
171+ exitCode = 1 ;
172+ const message = `Composite task failed: ${ String ( error ) } ` ;
173+ writeEmitter . fire ( `${ message } \r\n` ) ;
174+ this . channel . appendLine ( message ) ;
175+ } finally {
176+ closeEmitter . fire ( exitCode ) ;
177+ }
178+ } ,
179+ close : ( ) => {
180+ for ( const execution of activeExecutions ) {
181+ execution . terminate ( ) ;
182+ }
183+ }
184+ } ;
185+ return pty ;
186+ } ) ;
187+ }
188+
189+ private async runCompositeById ( commandId : string , activeExecutions : vscode . TaskExecution [ ] ) : Promise < { failed : boolean } > {
190+ const config = this . compositeConfigById . get ( commandId ) ;
191+ if ( ! config ) {
192+ this . channel . appendLine ( `Composite task not found: ${ commandId } ` ) ;
193+ return { failed : true } ;
194+ }
195+ return this . runCompositeCommands ( config . commandIds , config . parallel , activeExecutions , [ ] ) ;
196+ }
197+
198+ private async runCompositeCommands (
199+ commandIds : string [ ] ,
200+ parallel : boolean ,
201+ activeExecutions : vscode . TaskExecution [ ] ,
202+ stack : string [ ]
203+ ) : Promise < { failed : boolean } > {
204+ if ( parallel ) {
205+ const results = await Promise . all ( commandIds . map ( id => this . runCommandById ( id , activeExecutions , stack ) ) ) ;
206+ return { failed : results . some ( result => result . failed ) } ;
207+ }
208+
209+ let failed = false ;
210+ for ( const id of commandIds ) {
211+ const result = await this . runCommandById ( id , activeExecutions , stack ) ;
212+ if ( result . failed ) {
213+ failed = true ;
214+ }
215+ }
216+ return { failed } ;
217+ }
218+
219+ private async runCommandById (
220+ commandId : string ,
221+ activeExecutions : vscode . TaskExecution [ ] ,
222+ stack : string [ ]
223+ ) : Promise < { failed : boolean } > {
224+ if ( stack . includes ( commandId ) ) {
225+ this . channel . appendLine ( `Composite cycle detected: ${ [ ...stack , commandId ] . join ( ' -> ' ) } ` ) ;
226+ return { failed : true } ;
227+ }
228+ const execTask = this . execTaskById . get ( commandId ) ;
229+ if ( execTask ) {
230+ const execution = await vscode . tasks . executeTask ( execTask ) ;
231+ activeExecutions . push ( execution ) ;
232+ const failed = await this . waitForTaskEnd ( execution ) ;
233+ return { failed } ;
234+ }
235+
236+ const compositeConfig = this . compositeConfigById . get ( commandId ) ;
237+ if ( compositeConfig ) {
238+ return this . runCompositeCommands ( compositeConfig . commandIds , compositeConfig . parallel , activeExecutions , [ ...stack , commandId ] ) ;
239+ }
240+
241+ this . channel . appendLine ( `Composite dependency not found: ${ commandId } ` ) ;
242+ return { failed : true } ;
243+ }
244+
245+ private waitForTaskEnd ( execution : vscode . TaskExecution ) : Promise < boolean > {
246+ return new Promise ( resolve => {
247+ let exitCode : number | undefined ;
248+ const processDisposable = vscode . tasks . onDidEndTaskProcess ( event => {
249+ if ( event . execution === execution ) {
250+ exitCode = event . exitCode ;
251+ }
252+ } ) ;
253+ const disposable = vscode . tasks . onDidEndTask ( event => {
254+ if ( event . execution === execution ) {
255+ processDisposable . dispose ( ) ;
256+ disposable . dispose ( ) ;
257+ resolve ( exitCode !== undefined && exitCode !== 0 ) ;
258+ }
259+ } ) ;
260+ } ) ;
261+ }
95262}
0 commit comments