29
29
import com .cloudbees .jenkins .support .api .Component ;
30
30
import com .cloudbees .jenkins .support .api .SupportProvider ;
31
31
import com .cloudbees .jenkins .support .filter .ContentFilters ;
32
+ import edu .umd .cs .findbugs .annotations .NonNull ;
32
33
import hudson .Extension ;
33
34
import hudson .model .Api ;
34
35
import hudson .model .Failure ;
38
39
import hudson .security .Permission ;
39
40
import jakarta .servlet .ServletException ;
40
41
import jakarta .servlet .ServletOutputStream ;
42
+ import jakarta .servlet .http .HttpServletResponse ;
41
43
import java .io .File ;
42
44
import java .io .FileInputStream ;
43
45
import java .io .FileOutputStream ;
44
46
import java .io .IOException ;
47
+ import java .nio .file .Files ;
48
+ import java .nio .file .Path ;
49
+ import java .nio .file .Paths ;
45
50
import java .util .ArrayList ;
46
51
import java .util .Arrays ;
47
52
import java .util .Collections ;
52
57
import java .util .List ;
53
58
import java .util .Map ;
54
59
import java .util .Set ;
60
+ import java .util .UUID ;
61
+ import java .util .concurrent .ConcurrentHashMap ;
62
+ import java .util .concurrent .TimeUnit ;
55
63
import java .util .logging .Level ;
56
64
import java .util .logging .Logger ;
57
65
import java .util .stream .Collectors ;
58
66
import java .util .zip .ZipEntry ;
59
67
import java .util .zip .ZipOutputStream ;
60
68
import jenkins .model .Jenkins ;
69
+ import jenkins .util .ProgressiveRendering ;
70
+ import jenkins .util .Timer ;
71
+ import net .sf .json .JSON ;
61
72
import net .sf .json .JSONObject ;
62
73
import org .apache .commons .io .FileUtils ;
63
74
import org .jvnet .localizer .Localizable ;
64
75
import org .kohsuke .accmod .Restricted ;
65
76
import org .kohsuke .accmod .restrictions .NoExternalUse ;
66
77
import org .kohsuke .stapler .DataBoundConstructor ;
78
+ import org .kohsuke .stapler .HttpRedirect ;
67
79
import org .kohsuke .stapler .HttpResponse ;
68
80
import org .kohsuke .stapler .HttpResponses ;
69
81
import org .kohsuke .stapler .QueryParameter ;
@@ -92,6 +104,12 @@ public class SupportAction implements RootAction, StaplerProxy {
92
104
*/
93
105
private final Logger logger = Logger .getLogger (SupportAction .class .getName ());
94
106
107
+ private static final Path SUPPORT_BUNDLE_CREATION_FOLDER =
108
+ Paths .get (System .getProperty ("java.io.tmpdir" )).resolve ("support-bundle" );
109
+ public static final String SYNC_SUPPORT_BUNDLE = "support-bundle.zip" ;
110
+
111
+ private final Map <UUID , SupportBundleAsyncGenerator > generatorByTaskId = new ConcurrentHashMap <>();
112
+
95
113
@ Override
96
114
@ Restricted (NoExternalUse .class )
97
115
public Object getTarget () {
@@ -318,6 +336,63 @@ public void doGenerateAllBundles(StaplerRequest2 req, StaplerResponse2 rsp) thro
318
336
rsp .sendError (SC_BAD_REQUEST );
319
337
return ;
320
338
}
339
+ final List <Component > components = getComponents (req , json );
340
+ prepareBundle (rsp , components );
341
+ }
342
+
343
+ /**
344
+ * Generates a support bundle with selected components from the UI. in async
345
+ * @param req The stapler request
346
+ * @param rsp The stapler response
347
+ * @throws ServletException If an error occurred during form submission
348
+ * @throws IOException If an input or output exception occurs
349
+ */
350
+ @ RequirePOST
351
+ public HttpRedirect doGenerateBundleAsync (StaplerRequest2 req , StaplerResponse2 rsp )
352
+ throws ServletException , IOException {
353
+ JSONObject json = req .getSubmittedForm ();
354
+ if (!json .has ("components" )) {
355
+ rsp .sendError (SC_BAD_REQUEST );
356
+ return new HttpRedirect ("support" );
357
+ }
358
+ final List <Component > components = getComponents (req , json );
359
+ UUID taskId = UUID .randomUUID ();
360
+
361
+ // There are some components that need the request components to be processed
362
+ // these components cannot be processed async
363
+ // so process them first and then process the other components async
364
+ List <Component > syncComponent =
365
+ components .stream ().filter (c -> !c .canBeGeneratedAsync ()).toList ();
366
+ if (!syncComponent .isEmpty ()) {
367
+ Path outputDir = SUPPORT_BUNDLE_CREATION_FOLDER .resolve (taskId .toString ());
368
+ if (!Files .exists (outputDir )) {
369
+ try {
370
+ Files .createDirectories (outputDir );
371
+ } catch (IOException e ) {
372
+ throw new IOException ("Failed to create directory: " + outputDir .toAbsolutePath (), e );
373
+ }
374
+ }
375
+ try (FileOutputStream fileOutputStream =
376
+ new FileOutputStream (new File (outputDir .toString (), SYNC_SUPPORT_BUNDLE ))) {
377
+ SupportPlugin .setRequesterAuthentication (Jenkins .getAuthentication2 ());
378
+ try {
379
+ SupportPlugin .writeBundleForSyncComponents (fileOutputStream , syncComponent );
380
+ } finally {
381
+ SupportPlugin .clearRequesterAuthentication ();
382
+ }
383
+ } finally {
384
+ logger .fine ("Processing support bundle sunc completed" );
385
+ }
386
+ }
387
+
388
+ // Process the remaining components that can be process async
389
+ SupportBundleAsyncGenerator supportBundleAsyncGenerator = new SupportBundleAsyncGenerator ();
390
+ supportBundleAsyncGenerator .init (taskId , components );
391
+ generatorByTaskId .put (taskId , supportBundleAsyncGenerator );
392
+ return new HttpRedirect ("progressPage?taskId=" + taskId );
393
+ }
394
+
395
+ private List <Component > getComponents (StaplerRequest2 req , JSONObject json ) throws IOException {
321
396
logger .fine ("Parsing request..." );
322
397
Set <String > remove = new HashSet <>();
323
398
for (Selection s : req .bindJSONToList (Selection .class , json .get ("components" ))) {
@@ -343,7 +418,7 @@ public void doGenerateAllBundles(StaplerRequest2 req, StaplerResponse2 rsp) thro
343
418
if (supportPlugin != null ) {
344
419
supportPlugin .setExcludedComponents (remove );
345
420
}
346
- prepareBundle ( rsp , components ) ;
421
+ return components ;
347
422
}
348
423
349
424
/**
@@ -439,4 +514,105 @@ public boolean isSelected() {
439
514
return selected ;
440
515
}
441
516
}
517
+
518
+ public ProgressiveRendering getGeneratorByTaskId (String taskId ) throws Exception {
519
+ return generatorByTaskId .get (UUID .fromString (taskId ));
520
+ }
521
+
522
+ public static class SupportBundleAsyncGenerator extends ProgressiveRendering {
523
+ private final Logger logger = Logger .getLogger (SupportAction .class .getName ());
524
+ private UUID taskId ;
525
+ private boolean isCompleted ;
526
+ private List <Component > components ;
527
+ private boolean supportBundleGenerationInProgress = false ;
528
+ private String supportBundleName ;
529
+
530
+ public SupportBundleAsyncGenerator init (UUID taskId , List <Component > components ) {
531
+ this .taskId = taskId ;
532
+ this .components = components ;
533
+ return this ;
534
+ }
535
+
536
+ @ Override
537
+ protected void compute () throws Exception {
538
+ if (supportBundleGenerationInProgress ) {
539
+ logger .fine ("Support bundle generation already in progress, for task id " + taskId );
540
+ return ;
541
+ }
542
+
543
+ this .supportBundleName = BundleFileName .generate ();
544
+ this .supportBundleGenerationInProgress = true ;
545
+ logger .fine ("Generating support bundle... task id " + taskId );
546
+ Path outputDir = SUPPORT_BUNDLE_CREATION_FOLDER .resolve (taskId .toString ());
547
+ if (!Files .exists (outputDir )) {
548
+ try {
549
+ Files .createDirectories (outputDir );
550
+ } catch (IOException e ) {
551
+ throw new IOException ("Failed to create directory: " + outputDir .toAbsolutePath (), e );
552
+ }
553
+ }
554
+
555
+ try (FileOutputStream fileOutputStream =
556
+ new FileOutputStream (new File (outputDir .toString (), supportBundleName ))) {
557
+ SupportPlugin .setRequesterAuthentication (Jenkins .getAuthentication2 ());
558
+ try {
559
+ SupportPlugin .writeBundle (fileOutputStream , components , this ::progress , outputDir );
560
+ } finally {
561
+ SupportPlugin .clearRequesterAuthentication ();
562
+ }
563
+ } finally {
564
+ logger .fine ("Processing support bundle async completed" );
565
+ }
566
+
567
+ isCompleted = true ;
568
+ }
569
+
570
+ @ NonNull
571
+ @ Override
572
+ protected JSON data () {
573
+ JSONObject json = new JSONObject ();
574
+ json .put ("isCompleted" , isCompleted );
575
+ json .put ("taskId" , String .valueOf (taskId ));
576
+ return json ;
577
+ }
578
+
579
+ public String getSupportBundleName () {
580
+ return supportBundleName ;
581
+ }
582
+ }
583
+
584
+ public void doDownloadBundle (@ QueryParameter ("taskId" ) String taskId , StaplerResponse2 rsp ) throws IOException {
585
+ String supportBundleName =
586
+ generatorByTaskId .get (UUID .fromString (taskId )).getSupportBundleName ();
587
+
588
+ File bundleFile = new File (SUPPORT_BUNDLE_CREATION_FOLDER + "/" + taskId + "/" + supportBundleName );
589
+ if (!bundleFile .exists ()) {
590
+ rsp .sendError (HttpServletResponse .SC_NOT_FOUND , "Support bundle file not found" );
591
+ return ;
592
+ }
593
+
594
+ rsp .setContentType ("application/zip" );
595
+ rsp .addHeader ("Content-Disposition" , "attachment; filename=" + supportBundleName );
596
+ try (ServletOutputStream outputStream = rsp .getOutputStream ()) {
597
+ Files .copy (bundleFile .toPath (), outputStream );
598
+ }
599
+
600
+ // Clean up temporary files after assembling the full bundle
601
+ Timer .get ()
602
+ .schedule (
603
+ () -> {
604
+ File outputDir = new File (SUPPORT_BUNDLE_CREATION_FOLDER + "/" + taskId );
605
+
606
+ try {
607
+ FileUtils .deleteDirectory (outputDir );
608
+ generatorByTaskId .remove (taskId );
609
+ logger .fine (() -> "Cleaned up temporary directory " + outputDir );
610
+
611
+ } catch (IOException e ) {
612
+ logger .log (Level .WARNING , () -> "Unable to delete " + outputDir );
613
+ }
614
+ },
615
+ 15 ,
616
+ TimeUnit .MINUTES );
617
+ }
442
618
}
0 commit comments