1 // Licensed to the .NET Foundation under one or more agreements. 2 // The .NET Foundation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 6 using System; 7 using System.Collections.Generic; 8 using System.Linq; 9 using System.Text; 10 using System.Threading; 11 using System.Runtime.CompilerServices; 12 using System.IO; 13 using System.Net; 14 using System.Diagnostics; 15 using System.Runtime.InteropServices; 16 17 namespace Microsoft.Test.Data.SqlClient 18 { 19 /// <summary> 20 /// allows user to manipulate %windir%\system32\drivers\etc\hosts 21 /// the hosts file must be reverted if changed even if test application crashes, thus inherit from CriticalFinalizerObject. Make sure the instance is disposed after its use. 22 /// The last dispose call on the active instance reverts the hosts file. 23 /// 24 /// Usage: 25 /// using (var hostsFile = new HostsFileManager()) 26 /// { 27 /// // use the hostsFile methods to add/remove entries 28 /// // simultaneous usage of HostsFileManager in two app domains or processes on the same machine is not allowed 29 /// } 30 /// </summary> 31 public sealed class HostsFileManager : IDisposable 32 { 33 // define global (machine-wide) lock instance 34 private static EventWaitHandle s_globalLock = new EventWaitHandle(true /* create as signalled */, EventResetMode.AutoReset, @"Global\HostsFileManagerLock"); 35 private static bool s_globalLockTaken; // set when global (machine-wide) lock is in use 36 37 private static int s_localUsageRefCount; 38 private static object s_localLock = new object(); 39 40 private static string s_hostsFilePath; 41 private static string s_backupPath; 42 private static bool s_hasBackup; 43 private static TextReader s_activeReader; 44 private static TextWriter s_activeWriter; 45 private static List<HostEntry> s_entriesCache; 46 47 private const string HostsFilePathUnderSystem32 = @"C:\Windows\System32\drivers\etc\hosts"; 48 private const string HostsFilePathUnderLinux = "/etc/hosts"; 49 private const string HostsFilePathUnderMacOS = "/private/etc/hosts"; 50 51 InitializeGlobal(ref bool mustRelease)52 private static void InitializeGlobal(ref bool mustRelease) 53 { 54 if (mustRelease) 55 { 56 // already initialized 57 return; 58 } 59 60 lock (s_localLock) 61 { 62 if (mustRelease) 63 { 64 // check again under lock 65 return; 66 } 67 68 if (s_localUsageRefCount > 0) 69 { 70 // initialized by another thread 71 ++s_localUsageRefCount; 72 return; 73 } 74 75 // first call to initialize in this app domain 76 // note: simultanious use of HostsFileManager is currently supported only within single AppDomain scope 77 78 // non-critical initialization goes first 79 if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 80 { 81 s_hostsFilePath = HostsFilePathUnderSystem32; 82 } 83 else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 84 { 85 s_hostsFilePath = HostsFilePathUnderLinux; 86 } 87 else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 88 { 89 s_hostsFilePath = HostsFilePathUnderMacOS; 90 } 91 92 s_backupPath = Path.Combine(Path.GetTempPath(), string.Format("Hosts_{0}.bak", Guid.NewGuid().ToString("N"))); 93 94 // try to get global lock 95 // note that once global lock is aquired, it must be released 96 try { } 97 finally 98 { 99 if (s_globalLock.WaitOne(0)) 100 { 101 s_globalLockTaken = true; 102 mustRelease = true; 103 ++s_localUsageRefCount; // increment ref count for the first thread using the manager 104 } 105 } 106 107 if (!s_globalLockTaken) 108 { 109 throw new InvalidOperationException("HostsFileManager cannot initialize because hosts file is in use by another instance of the manager in the same or a different process (concurrent access is not allowed)"); 110 } 111 112 // locked now, take snapshot of hosts file and save it as a backup 113 File.Copy(s_hostsFilePath, s_backupPath); 114 s_hasBackup = true; 115 116 // load the current entries 117 InternalRefresh(); 118 } 119 } 120 TerminateGlobal(ref bool originalMustRelease)121 private static void TerminateGlobal(ref bool originalMustRelease) 122 { 123 if (!originalMustRelease) 124 { 125 // already disposed 126 return; 127 } 128 129 lock (s_localLock) 130 { 131 if (!originalMustRelease) 132 { 133 // check again under lock 134 return; 135 } 136 137 // not yet disposed, do it now 138 if (s_localUsageRefCount > 1) 139 { 140 // still in use by another thread(s) 141 --s_localUsageRefCount; 142 return; 143 } 144 145 if (s_activeReader != null) 146 { 147 s_activeReader.Dispose(); 148 s_activeReader = null; 149 } 150 if (s_activeWriter != null) 151 { 152 s_activeWriter.Dispose(); 153 s_activeWriter = null; 154 } 155 bool deleteBackup = false; 156 if (s_hasBackup) 157 { 158 // revert the hosts file 159 File.Copy(s_backupPath, s_hostsFilePath, overwrite: true); 160 s_hasBackup = false; 161 deleteBackup = true; 162 } 163 164 // Note: if critical finalizer fails to revert the hosts file, the global lock might remain reset until the machine is rebooted. 165 // if this happens, Hosts file in unpredictable state so there is no point in running tests anyway 166 if (s_globalLockTaken) 167 { 168 try { } 169 finally 170 { 171 s_globalLock.Set(); 172 s_globalLockTaken = false; 173 --s_localUsageRefCount; // decrement local ref count 174 originalMustRelease = false; 175 } 176 } 177 178 // now we can destroy the backup 179 if (deleteBackup) 180 { 181 File.Delete(s_backupPath); 182 } 183 } 184 } 185 186 private bool _mustRelease; 187 private bool _disposed; 188 HostsFileManager()189 public HostsFileManager() 190 { 191 // lazy initialization 192 _mustRelease = false; 193 _disposed = false; 194 } 195 ~HostsFileManager()196 ~HostsFileManager() 197 { 198 Dispose(false); 199 } 200 Dispose()201 public void Dispose() 202 { 203 Dispose(true); 204 GC.SuppressFinalize(this); 205 } 206 Dispose(bool disposing)207 private void Dispose(bool disposing) 208 { 209 if (!_disposed) 210 { 211 _disposed = true; 212 TerminateGlobal(ref _mustRelease); 213 } 214 } 215 216 public class HostEntry 217 { HostEntry(string name, IPAddress address)218 public HostEntry(string name, IPAddress address) 219 { 220 ValidateName(name); 221 ValidateAddress(address); 222 223 this.Name = name; 224 this.Address = address; 225 } 226 227 public readonly string Name; 228 public readonly IPAddress Address; 229 } 230 231 // helper methods 232 233 // must be called under lock(_localLock) from each public API that uses static fields InitializeLocal()234 private void InitializeLocal() 235 { 236 if (_disposed) 237 { 238 throw new ObjectDisposedException(this.GetType().Name); 239 } 240 241 InitializeGlobal(ref _mustRelease); 242 } 243 244 private static readonly char[] s_whiteSpaceChars = new char[] { ' ', '\t' }; 245 ValidateName(string name)246 private static void ValidateName(string name) 247 { 248 if (string.IsNullOrEmpty(name) || name.IndexOfAny(s_whiteSpaceChars) >= 0) 249 { 250 throw new ArgumentException("name cannot be null or empty or have whitespace characters in it"); 251 } 252 } 253 ValidateAddress(IPAddress address)254 private static void ValidateAddress(IPAddress address) 255 { 256 ValidateNonNull(address, "address"); 257 258 if (address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork && 259 address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetworkV6) 260 { 261 throw new ArgumentException("only IPv4 or IPv6 addresses are allowed"); 262 } 263 } 264 265 private static void ValidateNonNull<T>(T value, string argName) where T : class 266 { 267 if (value == null) 268 { 269 throw new ArgumentNullException(argName); 270 } 271 } 272 TryParseLine(string line)273 private static HostEntry TryParseLine(string line) 274 { 275 line = line.Trim(); 276 if (line.StartsWith("#")) 277 { 278 // comment, ignore 279 return null; 280 } 281 282 string[] items = line.Split(s_whiteSpaceChars, StringSplitOptions.RemoveEmptyEntries); 283 if (items.Length == 0) 284 { 285 // empty or white-space only line - ignore 286 return null; 287 } 288 289 if (items.Length != 2) 290 { 291 Trace.WriteLine("Wrong entry in the hosts file (exactly two columns expected): \"" + line + "\""); 292 return null; 293 } 294 295 string name = items[1]; 296 IPAddress address; 297 if (!IPAddress.TryParse(items[0], out address)) 298 { 299 Trace.WriteLine("Wrong entry in the hosts file (cannot parse the IP address): \"" + line + "\""); 300 return null; 301 } 302 303 try 304 { 305 return new HostEntry(name, address); 306 } 307 catch (ArgumentException e) 308 { 309 Console.WriteLine("Wrong entry in the hosts file, cannot create host entry: " + e.Message); 310 return null; 311 } 312 } 313 NameMatch(HostEntry entry, string name)314 private bool NameMatch(HostEntry entry, string name) 315 { 316 ValidateNonNull(entry, "entry"); 317 ValidateName(name); 318 319 return string.Equals(entry.Name, name, StringComparison.OrdinalIgnoreCase); 320 } 321 322 // hosts file manipulation methods 323 324 // reloads the hosts file, must be called under lock(_localLock) InternalRefresh()325 private static void InternalRefresh() 326 { 327 List<HostEntry> entries = new List<HostEntry>(); 328 329 try 330 { 331 s_activeReader = new StreamReader(new FileStream(s_hostsFilePath, FileMode.Open)); 332 333 string line; 334 while ((line = s_activeReader.ReadLine()) != null) 335 { 336 HostEntry nextEntry = TryParseLine(line); 337 if (nextEntry != null) 338 { 339 entries.Add(nextEntry); 340 } 341 } 342 } 343 finally 344 { 345 if (s_activeReader != null) 346 { 347 s_activeReader.Dispose(); 348 s_activeReader = null; 349 } 350 } 351 352 s_entriesCache = entries; 353 } 354 355 // reloads the hosts file, must be called while still under lock(_localLock) InternalSave()356 private void InternalSave() 357 { 358 try 359 { 360 s_activeWriter = new StreamWriter(new FileStream(s_hostsFilePath, FileMode.Create)); 361 362 foreach (HostEntry entry in s_entriesCache) 363 { 364 s_activeWriter.WriteLine(" {0} {1}", entry.Address, entry.Name); 365 } 366 367 s_activeWriter.Flush(); 368 } 369 finally 370 { 371 if (s_activeWriter != null) 372 { 373 s_activeWriter.Dispose(); 374 s_activeWriter = null; 375 } 376 } 377 } 378 RemoveAll(string name)379 public int RemoveAll(string name) 380 { 381 lock (s_localLock) 382 { 383 InitializeLocal(); 384 ValidateName(name); 385 386 int removed = s_entriesCache.RemoveAll(entry => NameMatch(entry, name)); 387 388 if (removed > 0) 389 { 390 InternalSave(); 391 } 392 393 return removed; 394 } 395 } 396 EnumerateAddresses(string name)397 public IEnumerable<IPAddress> EnumerateAddresses(string name) 398 { 399 lock (s_localLock) 400 { 401 InitializeLocal(); 402 ValidateName(name); 403 404 return from entry in s_entriesCache where NameMatch(entry, name) select entry.Address; 405 } 406 } 407 Add(string name, IPAddress address)408 public void Add(string name, IPAddress address) 409 { 410 lock (s_localLock) 411 { 412 InitializeLocal(); 413 414 HostEntry entry = new HostEntry(name, address); // c-tor validates the arguments 415 s_entriesCache.Add(entry); 416 417 InternalSave(); 418 } 419 } 420 Add(HostEntry entry)421 public void Add(HostEntry entry) 422 { 423 lock (s_localLock) 424 { 425 InitializeLocal(); 426 ValidateNonNull(entry, "entry"); 427 428 s_entriesCache.Add(entry); 429 430 InternalSave(); 431 } 432 } 433 AddRange(string name, IEnumerable<IPAddress> addresses)434 public void AddRange(string name, IEnumerable<IPAddress> addresses) 435 { 436 lock (s_localLock) 437 { 438 InitializeLocal(); 439 ValidateName(name); 440 ValidateNonNull(addresses, "addresses"); 441 442 foreach (IPAddress address in addresses) 443 { 444 HostEntry entry = new HostEntry(name, address); 445 446 s_entriesCache.Add(entry); 447 } 448 449 InternalSave(); 450 } 451 } 452 AddRange(IEnumerable<HostEntry> entries)453 public void AddRange(IEnumerable<HostEntry> entries) 454 { 455 lock (s_localLock) 456 { 457 InitializeLocal(); 458 ValidateNonNull(entries, "entries"); 459 460 foreach (HostEntry entry in entries) 461 { 462 ValidateNonNull(entry, "entries element"); 463 464 s_entriesCache.Add(entry); 465 } 466 467 InternalSave(); 468 } 469 } 470 Clear()471 public void Clear() 472 { 473 lock (s_localLock) 474 { 475 InitializeLocal(); 476 477 s_entriesCache.Clear(); 478 479 InternalSave(); 480 } 481 } 482 } 483 } 484