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