11package io .jenkins .plugins .agent_build_history ;
22
3- import edu .umd .cs .findbugs .annotations .NonNull ;
43import edu .umd .cs .findbugs .annotations .SuppressFBWarnings ;
5- import hudson .Extension ;
64import hudson .model .AbstractBuild ;
75import hudson .model .Action ;
86import hudson .model .Computer ;
9- import hudson .model .Item ;
107import hudson .model .Job ;
118import hudson .model .Node ;
129import hudson .model .Run ;
13- import hudson .model .listeners .ItemListener ;
14- import hudson .model .listeners .RunListener ;
1510import hudson .util .RunList ;
16- import java .util .Collections ;
17- import java .util .HashMap ;
18- import java .util .Map ;
19- import java .util .Set ;
20- import java .util .TreeSet ;
21- import java .util .concurrent .TimeUnit ;
22- import java .util .logging .Level ;
23- import java .util .logging .Logger ;
2411import jenkins .model .Jenkins ;
25- import jenkins .model .NodeListener ;
2612import jenkins .util .Timer ;
2713import org .jenkinsci .plugins .workflow .cps .nodes .StepStartNode ;
2814import org .jenkinsci .plugins .workflow .flow .FlowExecution ;
3420import org .jenkinsci .plugins .workflow .support .steps .ExecutorStep ;
3521import org .kohsuke .accmod .Restricted ;
3622import org .kohsuke .accmod .restrictions .NoExternalUse ;
23+ import org .kohsuke .stapler .Stapler ;
24+ import org .kohsuke .stapler .StaplerRequest ;
25+
26+ import javax .servlet .http .Cookie ;
27+ import java .util .ArrayList ;
28+ import java .util .List ;
29+ import java .util .concurrent .TimeUnit ;
30+ import java .util .logging .Level ;
31+ import java .util .logging .Logger ;
3732
3833@ Restricted (NoExternalUse .class )
3934public class AgentBuildHistory implements Action {
4035
4136 private static final Logger LOGGER = Logger .getLogger (AgentBuildHistory .class .getName ());
42-
43- private static final Map <String , Set <AgentExecution >> agentExecutions = new HashMap <>();
44- private static final Map <Run <?, ?>, AgentExecution > agentExecutionsMap = new HashMap <>();
45-
4637 private final Computer computer ;
47-
38+ private int totalPages = 1 ;
4839 private static boolean loaded = false ;
49-
5040 private static boolean loadingComplete = false ;
5141
52- @ Extension
53- public static class HistoryRunListener extends RunListener <Run <?, ?>> {
54-
55- @ Override
56- public void onDeleted (Run run ) {
57- for (Set <AgentExecution > executions : agentExecutions .values ()) {
58- executions .removeIf (exec -> run == exec .getRun ());
59- }
60- agentExecutionsMap .remove (run );
61- }
42+ public AgentBuildHistory (Computer computer ) {
43+ this .computer = computer ;
44+ LOGGER .log (Level .CONFIG , () -> "Creating AgentBuildHistory for " + computer .getName ());
6245 }
6346
64- @ Extension
65- public static class HistoryItemListener extends ItemListener {
66-
67- @ Override
68- public void onDeleted (Item item ) {
69- if (item instanceof Job ) {
70- for (Set <AgentExecution > executions : agentExecutions .values ()) {
71- executions .removeIf ( exec -> exec .getRun ().getParent () == item );
47+ public static String getCookieValue (StaplerRequest req , String name , String defaultValue ) {
48+ Cookie [] cookies = req .getCookies ();
49+ if (cookies != null ) {
50+ for (Cookie cookie : cookies ) {
51+ if (cookie .getName ().equals (name )) {
52+ return cookie .getValue ();
7253 }
73- agentExecutionsMap .keySet ().removeIf (key -> key .getParent () == item );
7454 }
7555 }
76- }
77-
78- @ Extension
79- public static class HistoryNodeListener extends NodeListener {
80-
81- @ Override
82- protected void onDeleted (@ NonNull Node node ) {
83- agentExecutions .remove (node .getNodeName ());
84- }
85- }
86-
87- public AgentBuildHistory (Computer computer ) {
88- this .computer = computer ;
56+ return defaultValue ; // Fallback to default if cookie not found
8957 }
9058
9159 /*
@@ -94,33 +62,148 @@ public AgentBuildHistory(Computer computer) {
9462 public Computer getComputer () {
9563 return computer ;
9664 }
97-
65+ /*
66+ * used by jelly
67+ */
9868 public boolean isLoadingComplete () {
9969 return loadingComplete ;
10070 }
10171
72+ public static void setLoaded (boolean loaded ) {
73+ AgentBuildHistory .loaded = loaded ;
74+ AgentBuildHistory .loadingComplete = loaded ;
75+ }
76+
77+ public int getTotalPages () {
78+ return totalPages ;
79+ }
80+
10281 @ SuppressFBWarnings (value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD" )
10382 public RunListTable getHandler () {
10483 if (!loaded ) {
10584 loaded = true ;
10685 Timer .get ().schedule (AgentBuildHistory ::load , 0 , TimeUnit .SECONDS );
10786 }
10887 RunListTable runListTable = new RunListTable (computer .getName ());
109- runListTable .setRuns (getExecutions ());
88+ //Get Parameters from URL
89+ StaplerRequest req = Stapler .getCurrentRequest ();
90+ int page = req .getParameter ("page" ) != null ? Integer .parseInt (req .getParameter ("page" )) : 1 ;
91+ int pageSize = req .getParameter ("pageSize" ) != null ? Integer .parseInt (req .getParameter ("pageSize" )) : Integer .parseInt (getCookieValue (req , "pageSize" , "20" ));
92+ String sortColumn = req .getParameter ("sortColumn" ) != null ? req .getParameter ("sortColumn" ) : getCookieValue (req , "sortColumn" , "startTime" );
93+ String sortOrder = req .getParameter ("sortOrder" ) != null ? req .getParameter ("sortOrder" ) : getCookieValue (req , "sortOrder" , "desc" );
94+ //Update totalPages depending on pageSize
95+ int totalEntries = BuildHistoryFileManager .readIndexFile (computer .getName (), AgentBuildHistoryConfig .get ().getStorageDir ()).size ();
96+ totalPages = (int ) Math .ceil ((double ) totalEntries / pageSize );
97+
98+ LOGGER .finer ("Getting runs for node: " + computer .getName () + " page: " + page + " pageSize: " + pageSize + " sortColumn: " + sortColumn + " sortOrder: " + sortOrder );
99+
100+ int start = (page -1 )*pageSize ;
101+ runListTable .setRuns (getExecutionsForNode (computer .getName (), start , pageSize , sortColumn , sortOrder ));
110102 return runListTable ;
111103 }
112104
105+ public List <AgentExecution > getExecutionsForNode (String nodeName , int start , int limit , String sortColumn , String sortOrder ) {
106+ String storageDir = AgentBuildHistoryConfig .get ().getStorageDir ();
107+ List <String > indexLines = BuildHistoryFileManager .readIndexFile (nodeName , storageDir );
108+ LOGGER .finer ("Found " + indexLines .size () + " entries for node " + nodeName );
109+ if (indexLines .isEmpty ()) {
110+ return List .of ();
111+ }
112+ // Sort index lines based on start time or build
113+ indexLines .sort ((a , b ) -> {
114+ int comparison = 0 ;
115+ switch (sortColumn ){
116+ case "startTime" :
117+ long timeA = Long .parseLong (a .split (BuildHistoryFileManager .separator )[2 ]);
118+ long timeB = Long .parseLong (b .split (BuildHistoryFileManager .separator )[2 ]);
119+ comparison = Long .compare (timeA , timeB );
120+ break ;
121+ case "build" :
122+ comparison = a .split (BuildHistoryFileManager .separator )[0 ].compareTo (b .split (BuildHistoryFileManager .separator )[0 ]);
123+ if (comparison == 0 ) {
124+ // Only compare build numbers if the job names are the same
125+ int buildNumberA = Integer .parseInt (a .split (BuildHistoryFileManager .separator )[1 ]);
126+ int buildNumberB = Integer .parseInt (b .split (BuildHistoryFileManager .separator )[1 ]);
127+ comparison = Integer .compare (buildNumberA , buildNumberB );
128+ }
129+ break ;
130+ default :
131+ comparison = 0 ;
132+ }
133+ return sortOrder .equals ("asc" ) ? comparison : -comparison ;
134+ });
135+ // Apply pagination
136+ int end = Math .min (start + limit , indexLines .size ());
137+ List <String > page = indexLines .subList (start , end );
138+ List <AgentExecution > result = new ArrayList <>();
139+
140+ for (String line : page ) {
141+ String [] parts = line .split (BuildHistoryFileManager .separator );
142+ String jobName = parts [0 ];
143+ int buildNumber = Integer .parseInt (parts [1 ]);
144+ // Load execution using deserialization
145+ AgentExecution execution = loadSingleExecution (jobName , buildNumber );
146+ if (execution != null ) {
147+ result .add (execution );
148+ }
149+ }
150+ LOGGER .finer ("Returning " + result .size () + " entries for node " + nodeName );
151+ return result ;
152+ }
153+
154+ public static AgentExecution loadSingleExecution (String jobName , int buildNumber ) {
155+ Job <?, ?> job = Jenkins .get ().getItemByFullName (jobName , Job .class );
156+ Run <?, ?> run = null ;
157+ if (job != null ) {
158+ run = job .getBuildByNumber (buildNumber );
159+ }
160+ if (run == null ) {
161+ LOGGER .info ("Run not found for " + jobName + " #" + buildNumber );
162+ return null ;
163+ }
164+ LOGGER .finer ("Loading run " + run .getFullDisplayName ());
165+ AgentExecution execution = new AgentExecution (run );
166+
167+ if (run instanceof AbstractBuild ) {
168+ Node node = ((AbstractBuild <?, ?>) run ).getBuiltOn ();
169+ if (node != null ) {
170+ LOGGER .finer ("Loading AbstractBuild on node: " + node .getNodeName ());
171+ return execution ;
172+ }
173+ } else if (run instanceof WorkflowRun ) {
174+ WorkflowRun wfr = (WorkflowRun ) run ;
175+ LOGGER .finer ("Loading WorkflowRun: " + wfr .getFullDisplayName ());
176+ FlowExecution flowExecution = wfr .getExecution ();
177+ if (flowExecution != null ) {
178+ for (FlowNode flowNode : new DepthFirstScanner ().allNodes (flowExecution )) {
179+ if (! (flowNode instanceof StepStartNode )) {
180+ continue ;
181+ }
182+ for (WorkspaceActionImpl action : flowNode .getActions (WorkspaceActionImpl .class )) {
183+ StepStartNode startNode = (StepStartNode ) flowNode ;
184+ StepDescriptor descriptor = startNode .getDescriptor ();
185+ if (descriptor instanceof ExecutorStep .DescriptorImpl ) {
186+ String nodeName = action .getNode ();
187+ execution .addFlowNode (flowNode , nodeName );
188+ LOGGER .finer ("Loading WorkflowRun FlowNode on node: " + nodeName );
189+ }
190+ }
191+ }
192+ }
193+ }
194+ return execution ;
195+ }
196+
113197 private static void load () {
114- LOGGER .log (Level .FINE , () -> "Starting to load all runs" );
198+ LOGGER .log (Level .INFO , () -> "Starting to synchronize all runs" );
115199 RunList <Run <?, ?>> runList = RunList .fromJobs ((Iterable ) Jenkins .get ().allItems (Job .class ));
116200 runList .forEach (run -> {
117- LOGGER .log ( Level . FINER , () -> "Loading run " + run .getFullDisplayName ());
118- AgentExecution execution = getAgentExecution ( run );
201+ LOGGER .finer ( "Loading run " + run .getFullDisplayName ());
202+
119203 if (run instanceof AbstractBuild ) {
120204 Node node = ((AbstractBuild <?, ?>) run ).getBuiltOn ();
121205 if (node != null ) {
122- Set <AgentExecution > executions = loadExecutions (node .getNodeName ());
123- executions .add (execution );
206+ BuildHistoryFileManager .addRunToNodeIndex (node .getNodeName (), run , AgentBuildHistoryConfig .get ().getStorageDir ());
124207 }
125208 } else if (run instanceof WorkflowRun ) {
126209 WorkflowRun wfr = (WorkflowRun ) run ;
@@ -135,56 +218,23 @@ private static void load() {
135218 StepDescriptor descriptor = startNode .getDescriptor ();
136219 if (descriptor instanceof ExecutorStep .DescriptorImpl ) {
137220 String nodeName = action .getNode ();
138- execution .addFlowNode (flowNode , nodeName );
139- Set <AgentExecution > executions = loadExecutions (nodeName );
140- executions .add (execution );
221+ BuildHistoryFileManager .addRunToNodeIndex (nodeName , run , AgentBuildHistoryConfig .get ().getStorageDir ());
141222 }
142223 }
143224 }
144225 }
145226 }
146227 });
147228 loadingComplete = true ;
148- }
149-
150- private static Set <AgentExecution > loadExecutions (String computerName ) {
151- Set <AgentExecution > executions = agentExecutions .get (computerName );
152- if (executions == null ) {
153- LOGGER .log (Level .FINER , () -> "Creating executions for computer " + computerName );
154- executions = Collections .synchronizedSet (new TreeSet <>());
155- agentExecutions .put (computerName , executions );
156- }
157- return executions ;
158- }
159-
160- /* use by jelly */
161- public Set <AgentExecution > getExecutions () {
162- Set <AgentExecution > executions = agentExecutions .get (computer .getName ());
163- if (executions == null ) {
164- return Collections .emptySet ();
165- }
166- return Collections .unmodifiableSet (new TreeSet <>(executions ));
167- }
168-
169- @ NonNull
170- private static AgentExecution getAgentExecution (Run <?, ?> run ) {
171- AgentExecution exec = agentExecutionsMap .get (run );
172- if (exec == null ) {
173- LOGGER .log (Level .FINER , () -> "Creating execution for run " + run .getFullDisplayName ());
174- exec = new AgentExecution (run );
175- agentExecutionsMap .put (run , exec );
176- }
177- return exec ;
229+ LOGGER .log (Level .INFO , () -> "Synchronizing all runs complete" );
178230 }
179231
180232 public static void startJobExecution (Computer c , Run <?, ?> run ) {
181- loadExecutions (c .getName ()). add ( getAgentExecution ( run ));
233+ BuildHistoryFileManager . addRunToNodeIndex (c .getName (), run , AgentBuildHistoryConfig . get (). getStorageDir ( ));
182234 }
183235
184236 public static void startFlowNodeExecution (Computer c , WorkflowRun run , FlowNode node ) {
185- AgentExecution exec = getAgentExecution (run );
186- exec .addFlowNode (node , c .getName ());
187- loadExecutions (c .getName ()).add (exec );
237+ BuildHistoryFileManager .addRunToNodeIndex (c .getName (), run , AgentBuildHistoryConfig .get ().getStorageDir ());
188238 }
189239
190240 @ Override
0 commit comments