66import java .util .Collections ;
77import java .util .Date ;
88import java .util .List ;
9+ import net .jcip .annotations .GuardedBy ;
910import org .jenkins .plugins .lockableresources .Messages ;
1011import org .kohsuke .accmod .Restricted ;
1112import org .kohsuke .accmod .restrictions .NoExternalUse ;
1819@ Restricted (NoExternalUse .class )
1920public class LockedResourcesBuildAction implements Action {
2021
22+ @ GuardedBy ("logs" )
2123 private final List <LogEntry > logs = new ArrayList <>();
22- private final transient Object syncLogs = new Object ();
24+
25+ @ GuardedBy ("resourcesInUse" )
2326 private final List <String > resourcesInUse = new ArrayList <>();
2427
2528 public LockedResourcesBuildAction () {}
@@ -47,13 +50,13 @@ public List<String> getCurrentUsedResourceNames() {
4750 }
4851
4952 public void addUsedResources (List <String > resourceNames ) {
50- synchronized (resourcesInUse ) {
53+ synchronized (this . resourcesInUse ) {
5154 resourcesInUse .addAll (resourceNames );
5255 }
5356 }
5457
5558 public void removeUsedResources (List <String > resourceNames ) {
56- synchronized (resourcesInUse ) {
59+ synchronized (this . resourcesInUse ) {
5760 resourcesInUse .removeAll (resourceNames );
5861 }
5962 }
@@ -94,7 +97,6 @@ public static void addLog(
9497 }
9598
9699 public void addLog (final String resourceName , final String step , final String action ) {
97-
98100 synchronized (this .logs ) {
99101 this .logs .add (new LogEntry (step , action , resourceName ));
100102 }
@@ -107,6 +109,37 @@ public List<LogEntry> getReadOnlyLogs() {
107109 }
108110 }
109111
112+ @ Restricted (NoExternalUse .class )
113+ public List <String > getReadOnlyResourcesInUse () {
114+ synchronized (this .resourcesInUse ) {
115+ return new ArrayList <>(Collections .unmodifiableCollection (this .resourcesInUse ));
116+ }
117+ }
118+
119+ /** Copy constructor, primarily for {@link #writeReplace} */
120+ private LockedResourcesBuildAction (LockedResourcesBuildAction other ) {
121+ synchronized (other .logs ) {
122+ synchronized (other .resourcesInUse ) {
123+ this .logs .addAll (other .getReadOnlyLogs ());
124+ this .resourcesInUse .addAll (other .getReadOnlyResourcesInUse ());
125+ }
126+ }
127+ }
128+ /**
129+ * Ensure iteration during XStream marshalling is also synchronized,
130+ * otherwise we tend to get {@link java.util.ConcurrentModificationException}.<br/>
131+ *
132+ * The recommended approach is to copy-on-write the properties so a
133+ * snapshot can always be scraped consistently. But this can be costly
134+ * at run-time, so we use the next-best option: produce a consistent
135+ * replica of the current object for actual saving only on demand.<br/>
136+ *
137+ * This method is found by XStream via reflection.<br/>
138+ */
139+ protected synchronized Object writeReplace () {
140+ return new LockedResourcesBuildAction (this );
141+ }
142+
110143 public static class LogEntry {
111144
112145 private final String step ;
0 commit comments