1 // Copyright (c) Microsoft. All rights reserved.
2 // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3 
4 using System;
5 using System.Collections;
6 using System.Security.Permissions;
7 using System.Diagnostics;
8 
9 using Microsoft.Build.Framework;
10 using Microsoft.Build.BuildEngine.Shared;
11 
12 namespace Microsoft.Build.BuildEngine
13 {
14 
15     /// <summary>
16     /// A hashtable wrapper that defers copying until the data is written.
17     /// </summary>
18     internal sealed class CopyOnWriteHashtable : IDictionary, ICloneable
19     {
20         // Either of these can act as the true backing writeableData.
21         private Hashtable writeableData = null;
22 
23         // These are references to someone else's backing writeableData.
24         private Hashtable readonlyData = null;
25 
26         // This is used to synchronize access to the readonlyData and writeableData fields.
27         private object sharedLock;
28 
29         // Carry around the StringComparer when possible to make Clear less expensive.
30         StringComparer stringComparer = null;
31 
32 #region Construct
33         /// <summary>
34         /// Construct as a traditional data-backed hashtable.
35         /// </summary>
36         /// <param name="stringComparer"></param>
CopyOnWriteHashtable(StringComparer stringComparer)37         internal CopyOnWriteHashtable(StringComparer stringComparer)
38             : this(0, stringComparer)
39         {
40             this.sharedLock = new object();
41         }
42 
43         /// <summary>
44         /// Construct with specified initial capacity. If the capacity is known
45         /// up front, specifying it avoids unnecessary rehashing operations
46         /// </summary>
CopyOnWriteHashtable(int capacity, StringComparer stringComparer)47         internal CopyOnWriteHashtable(int capacity, StringComparer stringComparer)
48         {
49             ErrorUtilities.VerifyThrowArgumentNull(stringComparer, "stringComparer");
50             this.sharedLock = new object();
51 
52             if (capacity == 0)
53             {
54                 // Actually 0 tells the Hashtable to use its default, but let's not rely on that
55                 writeableData = new Hashtable(stringComparer);
56             }
57             else
58             {
59                 writeableData = new Hashtable(capacity, stringComparer);
60             }
61             readonlyData = null;
62             this.stringComparer = stringComparer;
63         }
64 
65         /// <summary>
66         /// Construct over an IDictionary instance.
67         /// </summary>
68         /// <param name="dictionary"></param>
69         /// <param name="stringComparer">The string comparer to use.</param>
CopyOnWriteHashtable(IDictionary dictionary, StringComparer stringComparer)70         internal CopyOnWriteHashtable(IDictionary dictionary, StringComparer stringComparer)
71         {
72             ErrorUtilities.VerifyThrowArgumentNull(dictionary, "dictionary");
73             ErrorUtilities.VerifyThrowArgumentNull(stringComparer, "stringComparer");
74 
75             this.sharedLock = new object();
76             CopyOnWriteHashtable source = dictionary as CopyOnWriteHashtable;
77             if (source != null)
78             {
79                 if (source.stringComparer.GetHashCode() == stringComparer.GetHashCode())
80                 {
81                     // If we're copying another CopyOnWriteHashtable then we can defer the clone until later.
82                     ConstructFrom(source);
83                     return;
84                 }
85                 else
86                 {
87                     // Technically, it would be legal to fall through here and let a new hashtable be constructed.
88                     // However, Engine is supposed to use consistent case comparisons everywhere and so, for us,
89                     // this means a bug in the engine code somewhere.
90                     throw new InternalErrorException("Bug: Changing the case-sensitiveness of a copied hash-table.");
91                 }
92 
93             }
94 
95             // Can't defer this because we don't control what gets written to the dictionary exogenously.
96             writeableData = new Hashtable(dictionary, stringComparer);
97             readonlyData = null;
98             this.stringComparer = stringComparer;
99         }
100 
101         /// <summary>
102         /// Construct a shallow copy over another instance of this class.
103         /// </summary>
104         /// <param name="that"></param>
CopyOnWriteHashtable(CopyOnWriteHashtable that)105         private CopyOnWriteHashtable(CopyOnWriteHashtable that)
106         {
107             this.sharedLock = new object();
108             ConstructFrom(that);
109         }
110 
111         /// <summary>
112         /// Implementation of construction logic.
113         /// </summary>
114         /// <param name="that"></param>
ConstructFrom(CopyOnWriteHashtable that)115         private void ConstructFrom(CopyOnWriteHashtable that)
116         {
117             lock (that.sharedLock)
118             {
119                 this.writeableData = null;
120 
121                 // If the source it was writeable, need to transform it into
122                 // read-only because we don't want subsequent writes to bleed through.
123                 if (that.writeableData != null)
124                 {
125                     that.readonlyData = that.writeableData;
126                     that.writeableData = null;
127                 }
128 
129                 this.readonlyData = that.readonlyData;
130                 this.stringComparer = that.stringComparer;
131             }
132         }
133 
134         /// <summary>
135         /// Whether or not this CopyOnWriteHashtable is currently a shallow or deep copy.
136         /// This state can change from true->false when this hashtable is written to.
137         /// </summary>
138         internal bool IsShallowCopy
139         {
140             get
141             {
142                 return this.readonlyData != null;
143             }
144         }
145 #endregion
146 #region Pass-through Hashtable methods.
Contains(Object key)147         public bool Contains(Object key) {return ReadOperation.Contains(key);}
Add(Object key, Object value)148         public void Add(Object key, Object value) {WriteOperation.Add(key, value);}
Clear()149         public void Clear()
150         {
151             lock (sharedLock)
152             {
153                 ErrorUtilities.VerifyThrow(stringComparer != null, "Should have a valid string comparer.");
154 
155                 writeableData = new Hashtable(stringComparer);
156                 readonlyData = null;
157             }
158         }
159 
IEnumerable.GetEnumerator()160         IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)ReadOperation).GetEnumerator(); }
GetEnumerator()161         public IDictionaryEnumerator GetEnumerator() {return ReadOperation.GetEnumerator();}
Remove(Object key)162         public void Remove(Object key) {WriteOperation.Remove(key);}
163         public bool IsFixedSize { get { return ReadOperation.IsFixedSize; }}
164         public bool IsReadOnly {get {return ReadOperation.IsFixedSize;}}
165         public ICollection Keys {get {return ReadOperation.Keys;}}
166         public ICollection Values {get {return ReadOperation.Values;}}
CopyTo(Array array, int arrayIndex)167         public void CopyTo(Array array, int arrayIndex) { ReadOperation.CopyTo(array, arrayIndex); }
168         public int Count{get { return ReadOperation.Count; }}
169         public bool IsSynchronized {get { return ReadOperation.IsSynchronized; }}
170         public Object SyncRoot {get { return ReadOperation.SyncRoot; }}
ContainsKey(Object key)171         public bool ContainsKey(Object key)    {return ReadOperation.Contains(key);}
172 
173         public Object this[Object key]
174         {
175             get
176             {
177                 return ReadOperation[key];
178             }
179             set
180             {
181                 lock (sharedLock)
182                 {
183                     if(writeableData != null)
184                     {
185                         writeableData[key] = value;
186                     }
187                     else
188                     {
189                         // Setting to exactly the same value? Skip the the Clone in this case.
190                         if (readonlyData[key] != value || (!readonlyData.ContainsKey(key)))
191                         {
192                             WriteOperation[key] = value;
193                         }
194                     }
195                 }
196 
197             }
198         }
199 #endregion
200 
201         /// <summary>
202         /// Clone this.
203         /// </summary>
204         /// <returns></returns>
Clone()205         public Object Clone()
206         {
207             return new CopyOnWriteHashtable(this);
208         }
209 
210         /// <summary>
211         /// Returns a hashtable instance for reading from.
212         /// </summary>
213         private Hashtable ReadOperation
214         {
215             get
216             {
217                 lock (sharedLock)
218                 {
219                     if (readonlyData != null)
220                     {
221                         return readonlyData;
222                     }
223 
224                     return writeableData;
225                 }
226             }
227         }
228 
229         /// <summary>
230         /// Returns a hashtable instance for writting to.
231         /// Clones the readonly hashtable if necessary to create a writeable version.
232         /// </summary>
233         private Hashtable WriteOperation
234         {
235             get
236             {
237                 lock (sharedLock)
238                 {
239                     if (writeableData == null)
240                     {
241                         writeableData = (Hashtable)readonlyData.Clone();
242                         readonlyData = null;
243                     }
244 
245                     return writeableData;
246                 }
247             }
248         }
249     }
250 }
251