Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

460 rader
22 KiB

  1. /* Copyright (C) 2008-2016 Peter Palotas, Jeffrey Jangli, Alexandr Normuradov
  2. *
  3. * Permission is hereby granted, free of charge, to any person obtaining a copy
  4. * of this software and associated documentation files (the "Software"), to deal
  5. * in the Software without restriction, including without limitation the rights
  6. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  7. * copies of the Software, and to permit persons to whom the Software is
  8. * furnished to do so, subject to the following conditions:
  9. *
  10. * The above copyright notice and this permission notice shall be included in
  11. * all copies or substantial portions of the Software.
  12. *
  13. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  14. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  15. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  16. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  17. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  18. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  19. * THE SOFTWARE.
  20. */
  21. using Alphaleonis.Win32.Network;
  22. using Microsoft.Win32.SafeHandles;
  23. using System;
  24. using System.Collections.Generic;
  25. using System.Diagnostics.CodeAnalysis;
  26. using System.IO;
  27. using System.Runtime.InteropServices;
  28. using System.Security;
  29. using System.Security.AccessControl;
  30. using System.Text;
  31. namespace Alphaleonis.Win32.Filesystem
  32. {
  33. /// <summary>Provides static methods to retrieve device resource information from a local or remote host.</summary>
  34. public static class Device
  35. {
  36. #region EnumerateDevices
  37. /// <summary>Enumerates all available devices on the local host.</summary>
  38. /// <returns><see cref="IEnumerable{DeviceInfo}"/> instances of type <see cref="DeviceGuid"/> from the local host.</returns>
  39. /// <param name="deviceGuid">One of the <see cref="DeviceGuid"/> devices.</param>
  40. [SecurityCritical]
  41. public static IEnumerable<DeviceInfo> EnumerateDevices(DeviceGuid deviceGuid)
  42. {
  43. return EnumerateDevices(null, deviceGuid);
  44. }
  45. /// <summary>Enumerates all available devices of type <see cref="DeviceGuid"/> on the local or remote host.</summary>
  46. /// <returns><see cref="IEnumerable{DeviceInfo}"/> instances of type <see cref="DeviceGuid"/> for the specified <paramref name="hostName"/>.</returns>
  47. /// <param name="hostName">The name of the local or remote host on which the device resides. <see langword="null"/> refers to the local host.</param>
  48. /// <param name="deviceGuid">One of the <see cref="DeviceGuid"/> devices.</param>
  49. [SecurityCritical]
  50. public static IEnumerable<DeviceInfo> EnumerateDevices(string hostName, DeviceGuid deviceGuid)
  51. {
  52. return EnumerateDevicesCore(null, hostName, deviceGuid);
  53. }
  54. #endregion // EnumerateDevices
  55. #region Internal Methods
  56. #region EnumerateDevicesCore
  57. /// <summary>Enumerates all available devices on the local or remote host.</summary>
  58. [SecurityCritical]
  59. internal static IEnumerable<DeviceInfo> EnumerateDevicesCore(SafeHandle safeHandle, string hostName, DeviceGuid deviceInterfaceGuid)
  60. {
  61. var callerHandle = safeHandle != null;
  62. var deviceGuid = new Guid(Utils.GetEnumDescription(deviceInterfaceGuid));
  63. // CM_Connect_Machine()
  64. // MSDN Note: Beginning in Windows 8 and Windows Server 2012 functionality to access remote machines has been removed.
  65. // You cannot access remote machines when running on these versions of Windows.
  66. // http://msdn.microsoft.com/en-us/library/windows/hardware/ff537948%28v=vs.85%29.aspx
  67. SafeCmConnectMachineHandle safeMachineHandle;
  68. var lastError = NativeMethods.CM_Connect_Machine(Path.LocalToUncCore(Host.GetUncName(hostName), false, false, false), out safeMachineHandle);
  69. if (safeMachineHandle != null && safeMachineHandle.IsInvalid)
  70. {
  71. safeMachineHandle.Close();
  72. NativeError.ThrowException(lastError, Resources.Handle_Is_Invalid);
  73. }
  74. using (safeMachineHandle)
  75. {
  76. // Start at the "Root" of the device tree of the specified machine.
  77. if (!callerHandle)
  78. safeHandle = NativeMethods.SetupDiGetClassDevsEx(ref deviceGuid, IntPtr.Zero, IntPtr.Zero,
  79. NativeMethods.SetupDiGetClassDevsExFlags.Present |
  80. NativeMethods.SetupDiGetClassDevsExFlags.DeviceInterface,
  81. IntPtr.Zero, hostName, IntPtr.Zero);
  82. if (safeHandle != null && safeHandle.IsInvalid)
  83. {
  84. safeHandle.Close();
  85. NativeError.ThrowException(Marshal.GetLastWin32Error(), Resources.Handle_Is_Invalid);
  86. }
  87. try
  88. {
  89. uint memberInterfaceIndex = 0;
  90. var deviceInterfaceData = CreateDeviceInterfaceDataInstance();
  91. // Start enumerating Device Interfaces.
  92. while (NativeMethods.SetupDiEnumDeviceInterfaces(safeHandle, IntPtr.Zero, ref deviceGuid, memberInterfaceIndex++, ref deviceInterfaceData))
  93. {
  94. lastError = Marshal.GetLastWin32Error();
  95. if (lastError != Win32Errors.NO_ERROR)
  96. NativeError.ThrowException(lastError, hostName);
  97. var deviceInfoData = CreateDeviceInfoDataInstance();
  98. var deviceInterfaceDetailData = GetDeviceInterfaceDetailDataInstance(safeHandle, deviceInterfaceData, deviceInfoData);
  99. // Get device interace details.
  100. if (!NativeMethods.SetupDiGetDeviceInterfaceDetail(safeHandle, ref deviceInterfaceData, ref deviceInterfaceDetailData, NativeMethods.DefaultFileBufferSize, IntPtr.Zero, ref deviceInfoData))
  101. {
  102. lastError = Marshal.GetLastWin32Error();
  103. if (lastError != Win32Errors.NO_ERROR)
  104. NativeError.ThrowException(lastError, hostName);
  105. }
  106. // Create DeviceInfo instance.
  107. // Set DevicePath property of DeviceInfo instance.
  108. var deviceInfo = new DeviceInfo(hostName) { DevicePath = deviceInterfaceDetailData.DevicePath };
  109. // Current InstanceId is at the "USBSTOR" level, so we
  110. // need up "move up" one level to get to the "USB" level.
  111. uint ptrPrevious;
  112. // CM_Get_Parent_Ex()
  113. // Note: Using this function to access remote machines is not supported
  114. // beginning with Windows 8 and Windows Server 2012, as this functionality has been removed.
  115. // http://msdn.microsoft.com/en-us/library/windows/hardware/ff538615%28v=vs.85%29.aspx
  116. lastError = NativeMethods.CM_Get_Parent_Ex(out ptrPrevious, deviceInfoData.DevInst, 0, safeMachineHandle);
  117. if (lastError != Win32Errors.CR_SUCCESS)
  118. NativeError.ThrowException(lastError, hostName);
  119. // Now we get the InstanceID of the USB level device.
  120. using (var safeBuffer = new SafeGlobalMemoryBufferHandle(NativeMethods.DefaultFileBufferSize))
  121. {
  122. // CM_Get_Device_ID_Ex()
  123. // Note: Using this function to access remote machines is not supported beginning with Windows 8 and Windows Server 2012,
  124. // as this functionality has been removed.
  125. // http://msdn.microsoft.com/en-us/library/windows/hardware/ff538411%28v=vs.85%29.aspx
  126. lastError = NativeMethods.CM_Get_Device_ID_Ex(deviceInfoData.DevInst, safeBuffer, (uint)safeBuffer.Capacity, 0, safeMachineHandle);
  127. if (lastError != Win32Errors.CR_SUCCESS)
  128. NativeError.ThrowException(lastError, hostName);
  129. // Add to instance.
  130. deviceInfo.InstanceId = safeBuffer.PtrToStringUni();
  131. }
  132. #region Get Registry Properties
  133. using (var safeBuffer = new SafeGlobalMemoryBufferHandle(NativeMethods.DefaultFileBufferSize))
  134. {
  135. uint regType;
  136. string dataString;
  137. var safeBufferCapacity = (uint) safeBuffer.Capacity;
  138. if (NativeMethods.SetupDiGetDeviceRegistryProperty(safeHandle, ref deviceInfoData, NativeMethods.SetupDiGetDeviceRegistryPropertyEnum.BaseContainerId, out regType, safeBuffer, safeBufferCapacity, IntPtr.Zero))
  139. {
  140. dataString = safeBuffer.PtrToStringUni();
  141. if (!Utils.IsNullOrWhiteSpace(dataString))
  142. deviceInfo.BaseContainerId = new Guid(dataString);
  143. }
  144. if (NativeMethods.SetupDiGetDeviceRegistryProperty(safeHandle, ref deviceInfoData, NativeMethods.SetupDiGetDeviceRegistryPropertyEnum.ClassGuid, out regType, safeBuffer, safeBufferCapacity, IntPtr.Zero))
  145. {
  146. dataString = safeBuffer.PtrToStringUni();
  147. if (!Utils.IsNullOrWhiteSpace(dataString))
  148. deviceInfo.ClassGuid = new Guid(dataString);
  149. }
  150. if (NativeMethods.SetupDiGetDeviceRegistryProperty(safeHandle, ref deviceInfoData, NativeMethods.SetupDiGetDeviceRegistryPropertyEnum.Class, out regType, safeBuffer, safeBufferCapacity, IntPtr.Zero))
  151. deviceInfo.Class = safeBuffer.PtrToStringUni();
  152. if (NativeMethods.SetupDiGetDeviceRegistryProperty(safeHandle, ref deviceInfoData, NativeMethods.SetupDiGetDeviceRegistryPropertyEnum.CompatibleIds, out regType, safeBuffer, safeBufferCapacity, IntPtr.Zero))
  153. deviceInfo.CompatibleIds = safeBuffer.PtrToStringUni();
  154. if (NativeMethods.SetupDiGetDeviceRegistryProperty(safeHandle, ref deviceInfoData, NativeMethods.SetupDiGetDeviceRegistryPropertyEnum.DeviceDescription, out regType, safeBuffer, safeBufferCapacity, IntPtr.Zero))
  155. deviceInfo.DeviceDescription = safeBuffer.PtrToStringUni();
  156. if (NativeMethods.SetupDiGetDeviceRegistryProperty(safeHandle, ref deviceInfoData, NativeMethods.SetupDiGetDeviceRegistryPropertyEnum.Driver, out regType, safeBuffer, safeBufferCapacity, IntPtr.Zero))
  157. deviceInfo.Driver = safeBuffer.PtrToStringUni();
  158. if (NativeMethods.SetupDiGetDeviceRegistryProperty(safeHandle, ref deviceInfoData, NativeMethods.SetupDiGetDeviceRegistryPropertyEnum.EnumeratorName, out regType, safeBuffer, safeBufferCapacity, IntPtr.Zero))
  159. deviceInfo.EnumeratorName = safeBuffer.PtrToStringUni();
  160. if (NativeMethods.SetupDiGetDeviceRegistryProperty(safeHandle, ref deviceInfoData, NativeMethods.SetupDiGetDeviceRegistryPropertyEnum.FriendlyName, out regType, safeBuffer, safeBufferCapacity, IntPtr.Zero))
  161. deviceInfo.FriendlyName = safeBuffer.PtrToStringUni();
  162. if (NativeMethods.SetupDiGetDeviceRegistryProperty(safeHandle, ref deviceInfoData, NativeMethods.SetupDiGetDeviceRegistryPropertyEnum.HardwareId, out regType, safeBuffer, safeBufferCapacity, IntPtr.Zero))
  163. deviceInfo.HardwareId = safeBuffer.PtrToStringUni();
  164. if (NativeMethods.SetupDiGetDeviceRegistryProperty(safeHandle, ref deviceInfoData, NativeMethods.SetupDiGetDeviceRegistryPropertyEnum.LocationInformation, out regType, safeBuffer, safeBufferCapacity, IntPtr.Zero))
  165. deviceInfo.LocationInformation = safeBuffer.PtrToStringUni();
  166. if (NativeMethods.SetupDiGetDeviceRegistryProperty(safeHandle, ref deviceInfoData, NativeMethods.SetupDiGetDeviceRegistryPropertyEnum.LocationPaths, out regType, safeBuffer, safeBufferCapacity, IntPtr.Zero))
  167. deviceInfo.LocationPaths = safeBuffer.PtrToStringUni();
  168. if (NativeMethods.SetupDiGetDeviceRegistryProperty(safeHandle, ref deviceInfoData, NativeMethods.SetupDiGetDeviceRegistryPropertyEnum.Manufacturer, out regType, safeBuffer, safeBufferCapacity, IntPtr.Zero))
  169. deviceInfo.Manufacturer = safeBuffer.PtrToStringUni();
  170. if (NativeMethods.SetupDiGetDeviceRegistryProperty(safeHandle, ref deviceInfoData, NativeMethods.SetupDiGetDeviceRegistryPropertyEnum.PhysicalDeviceObjectName, out regType, safeBuffer, safeBufferCapacity, IntPtr.Zero))
  171. deviceInfo.PhysicalDeviceObjectName = safeBuffer.PtrToStringUni();
  172. if (NativeMethods.SetupDiGetDeviceRegistryProperty(safeHandle, ref deviceInfoData, NativeMethods.SetupDiGetDeviceRegistryPropertyEnum.Service, out regType, safeBuffer, safeBufferCapacity, IntPtr.Zero))
  173. deviceInfo.Service = safeBuffer.PtrToStringUni();
  174. }
  175. #endregion // Get Registry Properties
  176. yield return deviceInfo;
  177. // Get new structure instance.
  178. deviceInterfaceData = CreateDeviceInterfaceDataInstance();
  179. }
  180. }
  181. finally
  182. {
  183. // Handle is ours, dispose.
  184. if (!callerHandle && safeHandle != null)
  185. safeHandle.Close();
  186. }
  187. }
  188. }
  189. #endregion // EnumerateDevicesCore
  190. #region GetLinkTargetInfoCore
  191. /// <summary>Get information about the target of a mount point or symbolic link on an NTFS file system.</summary>
  192. [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Disposing is controlled.")]
  193. [SecurityCritical]
  194. internal static LinkTargetInfo GetLinkTargetInfoCore(SafeFileHandle safeHandle)
  195. {
  196. // Start with a large buffer to prevent a 2nd call.
  197. // MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16384
  198. uint bytesReturned = 4*NativeMethods.DefaultFileBufferSize;
  199. using (var safeBuffer = new SafeGlobalMemoryBufferHandle((int) bytesReturned))
  200. {
  201. while (true)
  202. {
  203. // DeviceIoControlMethod.Buffered = 0,
  204. // DeviceIoControlFileDevice.FileSystem = 9
  205. // FsctlGetReparsePoint = (DeviceIoControlFileDevice.FileSystem << 16) | (42 << 2) | DeviceIoControlMethod.Buffered | (0 << 14)
  206. if (!NativeMethods.DeviceIoControl(safeHandle, ((9 << 16) | (42 << 2) | 0 | (0 << 14)), IntPtr.Zero, 0, safeBuffer, (uint) safeBuffer.Capacity, out bytesReturned, IntPtr.Zero))
  207. {
  208. var lastError = Marshal.GetLastWin32Error();
  209. switch ((uint) lastError)
  210. {
  211. case Win32Errors.ERROR_MORE_DATA:
  212. case Win32Errors.ERROR_INSUFFICIENT_BUFFER:
  213. if (safeBuffer.Capacity < bytesReturned)
  214. {
  215. safeBuffer.Close();
  216. break;
  217. }
  218. NativeError.ThrowException(lastError);
  219. break;
  220. }
  221. }
  222. else
  223. break;
  224. }
  225. var marshalReparseBuffer = (int) Marshal.OffsetOf(typeof(NativeMethods.ReparseDataBufferHeader), "data");
  226. var header = safeBuffer.PtrToStructure<NativeMethods.ReparseDataBufferHeader>(0);
  227. var dataOffset = (int) (marshalReparseBuffer + (header.ReparseTag == ReparsePointTag.MountPoint
  228. ? Marshal.OffsetOf(typeof (NativeMethods.MountPointReparseBuffer), "data")
  229. : Marshal.OffsetOf(typeof (NativeMethods.SymbolicLinkReparseBuffer), "data")).ToInt64());
  230. var dataBuffer = new byte[bytesReturned - dataOffset];
  231. switch (header.ReparseTag)
  232. {
  233. case ReparsePointTag.MountPoint:
  234. var mountPoint = safeBuffer.PtrToStructure<NativeMethods.MountPointReparseBuffer>(marshalReparseBuffer);
  235. safeBuffer.CopyTo(dataOffset, dataBuffer, 0, dataBuffer.Length);
  236. return new LinkTargetInfo(
  237. Encoding.Unicode.GetString(dataBuffer, mountPoint.SubstituteNameOffset, mountPoint.SubstituteNameLength),
  238. Encoding.Unicode.GetString(dataBuffer, mountPoint.PrintNameOffset, mountPoint.PrintNameLength));
  239. case ReparsePointTag.SymLink:
  240. var symLink = safeBuffer.PtrToStructure<NativeMethods.SymbolicLinkReparseBuffer>(marshalReparseBuffer);
  241. safeBuffer.CopyTo(dataOffset, dataBuffer, 0, dataBuffer.Length);
  242. return new SymbolicLinkTargetInfo(
  243. Encoding.Unicode.GetString(dataBuffer, symLink.SubstituteNameOffset, symLink.SubstituteNameLength),
  244. Encoding.Unicode.GetString(dataBuffer, symLink.PrintNameOffset, symLink.PrintNameLength), symLink.Flags);
  245. default:
  246. throw new UnrecognizedReparsePointException();
  247. }
  248. }
  249. }
  250. #endregion // GetLinkTargetInfoCore
  251. #region ToggleCompressionCore
  252. /// <summary>Sets the NTFS compression state of a file or directory on a volume whose file system supports per-file and per-directory compression.</summary>
  253. /// <param name="isFolder">Specifies that <paramref name="path"/> is a file or directory.</param>
  254. /// <param name="transaction">The transaction.</param>
  255. /// <param name="path">A path that describes a folder or file to compress or decompress.</param>
  256. /// <param name="compress"><see langword="true"/> = compress, <see langword="false"/> = decompress</param>
  257. /// <param name="pathFormat">Indicates the format of the path parameter(s).</param>
  258. [SecurityCritical]
  259. internal static void ToggleCompressionCore(bool isFolder, KernelTransaction transaction, string path, bool compress, PathFormat pathFormat)
  260. {
  261. using (var handle = File.CreateFileCore(transaction, path, isFolder ? ExtendedFileAttributes.BackupSemantics : ExtendedFileAttributes.Normal, null, FileMode.Open, FileSystemRights.Modify, FileShare.None, true, pathFormat))
  262. {
  263. // DeviceIoControlMethod.Buffered = 0,
  264. // DeviceIoControlFileDevice.FileSystem = 9
  265. // FsctlSetCompression = (DeviceIoControlFileDevice.FileSystem << 16) | (16 << 2) | DeviceIoControlMethod.Buffered | ((FileAccess.Read | FileAccess.Write) << 14)
  266. // 0 = Decompress, 1 = Compress.
  267. InvokeIoControlUnknownSize(handle, ((9 << 16) | (16 << 2) | 0 | ((uint)(FileAccess.Read | FileAccess.Write) << 14)), (compress) ? 1 : 0);
  268. }
  269. }
  270. #endregion // ToggleCompressionCore
  271. #region Private
  272. #region CreateDeviceInfoDataInstance
  273. /// <summary>Builds a DeviceInfo Data structure.</summary>
  274. /// <returns>An initialized NativeMethods.SP_DEVINFO_DATA instance.</returns>
  275. [SecurityCritical]
  276. private static NativeMethods.SP_DEVINFO_DATA CreateDeviceInfoDataInstance()
  277. {
  278. var did = new NativeMethods.SP_DEVINFO_DATA();
  279. did.cbSize = (uint) Marshal.SizeOf(did);
  280. return did;
  281. }
  282. #endregion // CreateDeviceInfoDataInstance
  283. #region CreateDeviceInterfaceDataInstance
  284. /// <summary>Builds a Device Interface Data structure.</summary>
  285. /// <returns>An initialized NativeMethods.SP_DEVICE_INTERFACE_DATA instance.</returns>
  286. [SecurityCritical]
  287. private static NativeMethods.SP_DEVICE_INTERFACE_DATA CreateDeviceInterfaceDataInstance()
  288. {
  289. var did = new NativeMethods.SP_DEVICE_INTERFACE_DATA();
  290. did.cbSize = (uint) Marshal.SizeOf(did);
  291. return did;
  292. }
  293. #endregion // CreateDeviceInterfaceDataInstance
  294. #region GetDeviceInterfaceDetailDataInstance
  295. /// <summary>Builds a Device Interface Detail Data structure.</summary>
  296. /// <returns>An initialized NativeMethods.SP_DEVICE_INTERFACE_DETAIL_DATA instance.</returns>
  297. [SecurityCritical]
  298. private static NativeMethods.SP_DEVICE_INTERFACE_DETAIL_DATA GetDeviceInterfaceDetailDataInstance(SafeHandle safeHandle, NativeMethods.SP_DEVICE_INTERFACE_DATA deviceInterfaceData, NativeMethods.SP_DEVINFO_DATA deviceInfoData)
  299. {
  300. // Build a Device Interface Detail Data structure.
  301. var didd = new NativeMethods.SP_DEVICE_INTERFACE_DETAIL_DATA
  302. {
  303. cbSize = (IntPtr.Size == 4) ? (uint) (Marshal.SystemDefaultCharSize + 4) : 8
  304. };
  305. // Get device interace details.
  306. if (!NativeMethods.SetupDiGetDeviceInterfaceDetail(safeHandle, ref deviceInterfaceData, ref didd, NativeMethods.DefaultFileBufferSize, IntPtr.Zero, ref deviceInfoData))
  307. {
  308. var lastError = Marshal.GetLastWin32Error();
  309. if (lastError != Win32Errors.NO_ERROR)
  310. NativeError.ThrowException(lastError);
  311. }
  312. return didd;
  313. }
  314. #endregion // GetDeviceInterfaceDetailDataInstance
  315. #region InvokeIoControlUnknownSize
  316. /// <summary>Repeatedly invokes InvokeIoControl with the specified input until enough memory has been allocated.</summary>
  317. [SecurityCritical]
  318. private static byte[] InvokeIoControlUnknownSize<TV>(SafeFileHandle handle, uint controlCode, TV input, uint increment = 128)
  319. {
  320. byte[] output;
  321. uint bytesReturned;
  322. var inputSize = (uint) Marshal.SizeOf(input);
  323. var outputLength = increment;
  324. do
  325. {
  326. output = new byte[outputLength];
  327. if (!NativeMethods.DeviceIoControl(handle, controlCode, input, inputSize, output, outputLength, out bytesReturned, IntPtr.Zero))
  328. {
  329. var lastError = Marshal.GetLastWin32Error();
  330. switch ((uint)lastError)
  331. {
  332. case Win32Errors.ERROR_MORE_DATA:
  333. case Win32Errors.ERROR_INSUFFICIENT_BUFFER:
  334. outputLength += increment;
  335. break;
  336. default:
  337. NativeError.ThrowException(lastError);
  338. break;
  339. }
  340. }
  341. else
  342. break;
  343. } while (true);
  344. // Return the result
  345. if (output.Length == bytesReturned)
  346. return output;
  347. var res = new byte[bytesReturned];
  348. Array.Copy(output, res, bytesReturned);
  349. return res;
  350. }
  351. #endregion // InvokeIoControlUnknownSize
  352. #endregion // Private
  353. #endregion // Internal Methods
  354. }
  355. }