Source code of Windows XP (NT5)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

642 lines
24 KiB

  1. //////////////////////////////////////////////////////////////////////////////
  2. //
  3. // Copyright (c) 2000 Microsoft Corporation
  4. //
  5. // Module Name:
  6. // ClusComp.cpp
  7. //
  8. // Description:
  9. // This file implements that function that is called by WinNT32.exe before
  10. // an upgrade to ensure that no incompatibilities occur as a result of the
  11. // upgrade. For example, in a cluster of two NT4 nodes, one node cannot
  12. // be upgraded to Whistler while the other is still at NT4. The user is
  13. // warned about such problems by this function.
  14. //
  15. // NOTE: This function is called by WinNT32.exe on the OS *before* an
  16. // upgrade. If OS version X is being upgraded to OS version X+1, then
  17. // the X+1 verion of this DLL is loaded on OS version X. To make sure
  18. // that this DLL can function properly in an downlevel OS, it is linked
  19. // against only the indispensible libraries.
  20. //
  21. // Header File:
  22. // There is no header file for this source file.
  23. //
  24. // Maintained By:
  25. // Vij Vasu (Vvasu) 25-JUL-2000
  26. // Created the original version.
  27. //
  28. //////////////////////////////////////////////////////////////////////////////
  29. //////////////////////////////////////////////////////////////////////////////
  30. // Include Files
  31. //////////////////////////////////////////////////////////////////////////////
  32. // Precompiled header for this DLL
  33. #include "pch.h"
  34. // For the compatibility check function and types
  35. #include <comp.h>
  36. // For the cluster API
  37. #include <clusapi.h>
  38. // For the names of several cluster service related registry keys and values
  39. #include "clusudef.h"
  40. //////////////////////////////////////////////////////////////////////////////
  41. // Forward declarations
  42. //////////////////////////////////////////////////////////////////////////////
  43. DWORD DwIsClusterServiceRegistered( bool & rfIsRegisteredOut );
  44. DWORD DwLoadString( UINT nStringIdIn, WCHAR *& rpszDestOut );
  45. DWORD DwWriteOSVersionInfo( const OSVERSIONINFO & rcosviOSVersionInfoIn );
  46. //////////////////////////////////////////////////////////////////////////////
  47. //++
  48. //
  49. // extern "C"
  50. // BOOL
  51. // ClusterUpgradeCompatibilityCheck()
  52. //
  53. // Description:
  54. // This function is called by WinNT32.exe before an upgrade to ensure that
  55. // no incompatibilities occur as a result of the upgrade. For example,
  56. // in a cluster of two NT4 nodes, one node cannot be upgraded to Whistler
  57. // while the other is still at NT4.
  58. //
  59. // Arguments:
  60. // PCOMPAIBILITYCALLBACK pfnCompatibilityCallbackIn
  61. // Points to the callback function used to supply compatibility
  62. // information to WinNT32.exe
  63. //
  64. // LPVOID pvContextIn
  65. // Pointer to the context buffer supplied by WinNT32.exe
  66. //
  67. // Return Value:
  68. // TRUE if there were no errors or no compatibility problems.
  69. // FALSE otherwise.
  70. //
  71. //--
  72. //////////////////////////////////////////////////////////////////////////////
  73. extern "C"
  74. BOOL ClusterUpgradeCompatibilityCheck(
  75. PCOMPAIBILITYCALLBACK pfnCompatibilityCallbackIn
  76. , LPVOID pvContextIn
  77. )
  78. {
  79. TraceFunc( "" );
  80. LogMsg( "Entering function " __FUNCTION__ "()" );
  81. BOOL fReturnValue = TRUE;
  82. bool fWarningRequired = true;
  83. DWORD dwError = ERROR_SUCCESS;
  84. do
  85. {
  86. typedef CSmartResource< CHandleTrait< HCLUSTER, BOOL, CloseCluster > > SmartClusterHandle;
  87. OSVERSIONINFO osviOSVersionInfo;
  88. SmartClusterHandle schClusterHandle;
  89. DWORD cchBufferSize = 256;
  90. osviOSVersionInfo.dwOSVersionInfoSize = sizeof( osviOSVersionInfo );
  91. //
  92. // First of all, get and store the OS version info into the registry.
  93. //
  94. // Cannot call VerifyVerionInfo as this requires Win2k.
  95. if ( GetVersionEx( &osviOSVersionInfo ) == FALSE )
  96. {
  97. // We could not get OS version info.
  98. // Show the warning, just in case.
  99. dwError = TW32( GetLastError() );
  100. TraceFlow1( "Error %#x occurred trying to get the OS version info.", dwError );
  101. LogMsg( "Error %#x occurred trying to get the OS version info.", dwError );
  102. break;
  103. } // if: GetVersionEx() failed
  104. // Write the OS version info to the registry. This data will be used later by ClusOCM
  105. // to figure out which OS version we are upgrading from.
  106. dwError = TW32( DwWriteOSVersionInfo( osviOSVersionInfo ) );
  107. if ( dwError != ERROR_SUCCESS )
  108. {
  109. TraceFlow1( "Error %#x occurred trying to store the OS version info. This is not a fatal error.", dwError );
  110. LogMsg( "Error %#x occurred trying to store the OS version info. This is not a fatal error.", dwError );
  111. // This is not a fatal error. So reset the error code.
  112. dwError = ERROR_SUCCESS;
  113. } // if: there was an error writing the OS version info
  114. else
  115. {
  116. TraceFlow( "The OS version info was successfully written to the registry." );
  117. } // else: the OS version info was successfully written to the registry
  118. // Check if the cluster service is registered.
  119. dwError = TW32( DwIsClusterServiceRegistered( fWarningRequired ) );
  120. if ( dwError != ERROR_SUCCESS )
  121. {
  122. // We could not get the state of the cluster service
  123. // Show the warning, just in case.
  124. TraceFlow1( "Error %#x occurred trying to check if the cluster service is registered.", dwError );
  125. LogMsg( "Error %#x occurred trying to check if the cluster service is registered.", dwError );
  126. break;
  127. } // if: DwIsClusterServiceRegistered() returned an error
  128. if ( !fWarningRequired )
  129. {
  130. // If the cluster service was not registered, no warning is needed.
  131. TraceFlow( "The cluster service is not registered." );
  132. LogMsg( "The cluster service is not registered." );
  133. break;
  134. } // if: no warning is required
  135. TraceFlow( "The cluster service is registered. Checking the node versions." );
  136. LogMsg( "The cluster service is registered. Checking the node versions." );
  137. // Check if this is an NT4 node
  138. if ( osviOSVersionInfo.dwMajorVersion < 5 )
  139. {
  140. TraceFlow( "This is an NT4 node." );
  141. LogMsg( "This is an NT4 node." );
  142. fWarningRequired = true;
  143. break;
  144. } // if: this is an NT4 node
  145. TraceFlow( "This is not an NT4 node." );
  146. // Check if the OS version is Whistler or if it is a non-NT OS
  147. if ( ( osviOSVersionInfo.dwPlatformId != VER_PLATFORM_WIN32_NT )
  148. || ( ( osviOSVersionInfo.dwMajorVersion >= 5 )
  149. && ( osviOSVersionInfo.dwMinorVersion >= 1 )
  150. )
  151. )
  152. {
  153. // If the OS not of the NT family or if the OS version of this
  154. // node is Whistler or greater, no warning is required.
  155. TraceFlow2(
  156. "The version of the OS on this node is %d.%d. So, this is Windows Whister or later (or is not running NT)."
  157. , osviOSVersionInfo.dwMajorVersion
  158. , osviOSVersionInfo.dwMinorVersion
  159. );
  160. TraceFlow( "No NT4 nodes can exist in this cluster." );
  161. LogMsg( "The version of the OS on this node is Windows Whister or later (or is not running NT)." );
  162. LogMsg( "No NT4 nodes can exist in this cluster." );
  163. fWarningRequired = false;
  164. break;
  165. } // if: the OS is not NT or if it is Win2k or greater
  166. TraceFlow( "This is not a Whistler node - this has to be a Windows 2000 node." );
  167. TraceFlow( "Trying to check if there are any NT4 nodes in the cluster." );
  168. //
  169. // Get the cluster version information
  170. //
  171. // Open a handle to the local cluster
  172. schClusterHandle.Assign( OpenCluster( NULL ) );
  173. if ( schClusterHandle.HHandle() == NULL )
  174. {
  175. // Show the warning, just to be safe.
  176. dwError = TW32( GetLastError() );
  177. TraceFlow1( "Error %#x occurred trying to get a handle to the cluster.", dwError );
  178. LogMsg( "Error %#x occurred trying to get information about the cluster.", dwError );
  179. break;
  180. } // if: we could not get the cluster handle
  181. TraceFlow( "OpenCluster() was successful." );
  182. // Get the cluster version info
  183. do
  184. {
  185. // Allocate the buffer - this memory is automatically freed when this object
  186. // goes out of scope ( or during the next iteration ).
  187. SmartSz sszClusterName( new WCHAR[ cchBufferSize ] );
  188. CLUSTERVERSIONINFO cviClusterVersionInfo;
  189. if ( sszClusterName.FIsEmpty() )
  190. {
  191. dwError = TW32( ERROR_NOT_ENOUGH_MEMORY );
  192. TraceFlow1( "Error %#x occurred while allocating a buffer for the cluster name.", dwError );
  193. LogMsg( "Error %#x occurred while allocating a buffer for the cluster name.", dwError );
  194. break;
  195. } // if: memory allocation failed
  196. TraceFlow( "Memory for the cluster name has been allocated." );
  197. cviClusterVersionInfo.dwVersionInfoSize = sizeof( cviClusterVersionInfo );
  198. dwError = GetClusterInformation(
  199. schClusterHandle.HHandle()
  200. , sszClusterName.PMem()
  201. , &cchBufferSize
  202. , &cviClusterVersionInfo
  203. );
  204. if ( dwError == ERROR_SUCCESS )
  205. {
  206. // A warning is required if this node version is less than Win2k or
  207. // if there is a node in the cluster whose version is less than Win2k
  208. // NOTE: cviClusterVersionInfo.MajorVersion is the OS version
  209. // while cviClusterVersionInfo.dwClusterHighestVersion is the cluster version.
  210. fWarningRequired =
  211. ( ( cviClusterVersionInfo.MajorVersion < 5 )
  212. || ( CLUSTER_GET_MAJOR_VERSION( cviClusterVersionInfo.dwClusterHighestVersion ) < NT5_MAJOR_VERSION )
  213. );
  214. if ( fWarningRequired )
  215. {
  216. TraceFlow( "There is at least one node in the cluster whose OS version is earlier than Windows 2000." );
  217. LogMsg( "There is at least one node in the cluster whose OS version is earlier than Windows 2000." );
  218. } // if: a warning will be shown
  219. else
  220. {
  221. TraceFlow( "The OS versions of all the nodes in the cluster are Windows 2000 or later." );
  222. LogMsg( "The OS versions of all the nodes in the cluster are Windows 2000 or later." );
  223. } // else: a warning will not be shown
  224. break;
  225. } // if: we got the cluster version info
  226. else
  227. {
  228. if ( dwError == ERROR_MORE_DATA )
  229. {
  230. // Insufficient buffer - try again
  231. ++cchBufferSize;
  232. dwError = ERROR_SUCCESS;
  233. TraceFlow1( "The buffer size is insufficient. Need %d bytes. Reallocating.", cchBufferSize );
  234. continue;
  235. } // if: the size of the buffer was insufficient
  236. // If we are here, something has gone wrong - show the warning
  237. TW32( dwError );
  238. TraceFlow1( "Error %#x occurred trying to get cluster information.", dwError );
  239. LogMsg( "Error %#x occurred trying to get cluster information.", dwError );
  240. break;
  241. } // else: we could not get the cluster version info
  242. }
  243. while( true ); // loop infinitely
  244. // We are done.
  245. break;
  246. }
  247. while( false ); // Dummy do-while loop to avoid gotos
  248. while ( fWarningRequired )
  249. {
  250. SmartSz sszWarningTitle;
  251. COMPATIBILITY_ENTRY ceCompatibilityEntry;
  252. TraceFlow( "The compatibility warning is required." );
  253. LogMsg( "The compatibility warning is required." );
  254. {
  255. WCHAR * pszWarningTitle = NULL;
  256. dwError = TW32( DwLoadString( IDS_ERROR_UPGRADE_OTHER_NODES, pszWarningTitle ) );
  257. if ( dwError != ERROR_SUCCESS )
  258. {
  259. // We cannot show the warning
  260. TraceFlow1( "Error %#x occurred trying to load the warning string.", dwError );
  261. LogMsg( "Error %#x occurred trying to show the warning.", dwError );
  262. break;
  263. } // if: the load string failed
  264. sszWarningTitle.Assign( pszWarningTitle );
  265. }
  266. //
  267. // Call the callback function
  268. //
  269. ceCompatibilityEntry.Description = sszWarningTitle.PMem();
  270. ceCompatibilityEntry.HtmlName = L"CompData\\ClusComp.htm";
  271. ceCompatibilityEntry.TextName = L"CompData\\ClusComp.txt";
  272. ceCompatibilityEntry.RegKeyName = NULL;
  273. ceCompatibilityEntry.RegValName = NULL ;
  274. ceCompatibilityEntry.RegValDataSize = 0;
  275. ceCompatibilityEntry.RegValData = NULL;
  276. ceCompatibilityEntry.SaveValue = NULL;
  277. ceCompatibilityEntry.Flags = 0;
  278. ceCompatibilityEntry.InfName = NULL;
  279. ceCompatibilityEntry.InfSection = NULL;
  280. TraceFlow( "About to call the compatibility callback function." );
  281. // This function returns TRUE if the compatibility warning data was successfully set.
  282. fReturnValue = pfnCompatibilityCallbackIn( &ceCompatibilityEntry, pvContextIn );
  283. TraceFlow1( "The compatibility callback function returned %d.", fReturnValue );
  284. break;
  285. } // while: we need to show the warning
  286. if ( !fWarningRequired )
  287. {
  288. TraceFlow( "The compatibility warning need not be shown." );
  289. LogMsg( "The compatibility warning need not be shown." );
  290. } // if: we did not need to show the warning
  291. LogMsg( "Exiting function ClusterUpgradeCompatibilityCheck(). Return value is %d.", fReturnValue );
  292. RETURN( fReturnValue );
  293. } //*** ClusterUpgradeCompatibilityCheck()
  294. /////////////////////////////////////////////////////////////////////////////
  295. //++
  296. //
  297. // DWORD
  298. // DwIsClusterServiceRegistered()
  299. //
  300. // Description:
  301. // This function determines whether the Cluster Service has been registered
  302. // with the Service Control Manager or not. It is not possible to use the
  303. // GetNodeClusterState() API to see if this node is a member of a cluster
  304. // or not, since this API was not available on NT4 SP3.
  305. //
  306. // Arguments:
  307. // bool & rfIsRegisteredOut
  308. // If true, Cluster Service (ClusSvc) is registered with the Service
  309. // Control Manager (SCM). Else, Cluster Service (ClusSvc) is not
  310. // registered with SCM
  311. //
  312. // Return Value:
  313. // ERROR_SUCCESS if all went well.
  314. // Other Win32 error codes on failure.
  315. //
  316. //--
  317. /////////////////////////////////////////////////////////////////////////////
  318. DWORD
  319. DwIsClusterServiceRegistered( bool & rfIsRegisteredOut )
  320. {
  321. TraceFunc( "" );
  322. DWORD dwError = ERROR_SUCCESS;
  323. // Initialize the output
  324. rfIsRegisteredOut = false;
  325. // dummy do-while loop to avoid gotos
  326. do
  327. {
  328. // Instantiate the SmartServiceHandle smart handle class.
  329. typedef CSmartResource< CHandleTrait< SC_HANDLE, BOOL, CloseServiceHandle, NULL > > SmartServiceHandle;
  330. // Connect to the Service Control Manager
  331. SmartServiceHandle shServiceMgr( OpenSCManager( NULL, NULL, GENERIC_READ ) );
  332. // Was the service control manager database opened successfully?
  333. if ( shServiceMgr.HHandle() == NULL )
  334. {
  335. dwError = TW32( GetLastError() );
  336. TraceFlow1( "Error %#x occurred trying open a handle to the service control manager.", dwError );
  337. LogMsg( "Error %#x occurred trying open a handle to the service control manager.", dwError );
  338. break;
  339. } // if: opening the SCM was unsuccessful
  340. // Open a handle to the Cluster Service.
  341. SmartServiceHandle shService( OpenService( shServiceMgr, L"ClusSvc", GENERIC_READ ) );
  342. // Was the handle to the service opened?
  343. if ( shService.HHandle() != NULL )
  344. {
  345. TraceFlow( "Successfully opened a handle to the cluster service. Therefore, it is registered." );
  346. rfIsRegisteredOut = true;
  347. break;
  348. } // if: handle to clussvc could be opened
  349. dwError = GetLastError();
  350. if ( dwError == ERROR_SERVICE_DOES_NOT_EXIST )
  351. {
  352. TraceFlow( "The cluster service is not registered." );
  353. dwError = ERROR_SUCCESS;
  354. break;
  355. } // if: the handle could not be opened because the service did not exist.
  356. // If we are here, then some error occurred.
  357. TW32( dwError );
  358. TraceFlow1( "Error %#x occurred trying open a handle to the cluster service.", dwError );
  359. LogMsg( "Error %#x occurred trying open a handle to the cluster service.", dwError );
  360. // Handles are closed by the CSmartHandle destructor.
  361. }
  362. while ( false ); // dummy do-while loop to avoid gotos
  363. RETURN( dwError );
  364. } //*** DwIsClusterServiceRegistered()
  365. //////////////////////////////////////////////////////////////////////////////
  366. //++
  367. //
  368. // DwLoadString()
  369. //
  370. // Description:
  371. // Allocate memory for and load a string from the string table.
  372. //
  373. // Arguments:
  374. // uiStringIdIn
  375. // Id of the string to look up
  376. //
  377. // rpszDestOut
  378. // Reference to the pointer that will hold the address of the
  379. // loaded string. The memory will have to be freed by the caller
  380. // by using the delete operator.
  381. //
  382. // Return Value:
  383. // S_OK
  384. // If the call succeeded
  385. //
  386. // Other Win32 error codes
  387. // If the call failed.
  388. //
  389. // Remarks:
  390. // This function cannot load a zero length string.
  391. //--
  392. //////////////////////////////////////////////////////////////////////////////
  393. DWORD
  394. DwLoadString(
  395. UINT nStringIdIn
  396. , WCHAR *& rpszDestOut
  397. )
  398. {
  399. TraceFunc( "" );
  400. DWORD dwError = ERROR_SUCCESS;
  401. UINT uiCurrentSize = 0;
  402. SmartSz sszCurrentString;
  403. UINT uiReturnedStringLen = 0;
  404. // Initialize the output.
  405. rpszDestOut = NULL;
  406. do
  407. {
  408. // Grow the current string by an arbitrary amount.
  409. uiCurrentSize += 256;
  410. sszCurrentString.Assign( new WCHAR[ uiCurrentSize ] );
  411. if ( sszCurrentString.FIsEmpty() )
  412. {
  413. dwError = TW32( ERROR_NOT_ENOUGH_MEMORY );
  414. TraceFlow2( "Error %#x occurred trying allocate memory for string (string id is %d).", dwError, nStringIdIn );
  415. LogMsg( "Error %#x occurred trying allocate memory for string (string id is %d).", dwError, nStringIdIn );
  416. break;
  417. } // if: the memory allocation has failed
  418. uiReturnedStringLen = ::LoadString(
  419. g_hInstance
  420. , nStringIdIn
  421. , sszCurrentString.PMem()
  422. , uiCurrentSize
  423. );
  424. if ( uiReturnedStringLen == 0 )
  425. {
  426. dwError = TW32( GetLastError() );
  427. TraceFlow2( "Error %#x occurred trying load string (string id is %d).", dwError, nStringIdIn );
  428. LogMsg( "Error %#x occurred trying load string (string id is %d).", dwError, nStringIdIn );
  429. break;
  430. } // if: LoadString() had an error
  431. ++uiReturnedStringLen;
  432. }
  433. while( uiCurrentSize <= uiReturnedStringLen );
  434. if ( dwError == ERROR_SUCCESS )
  435. {
  436. // Detach the smart pointer from the string, so that it is not freed by this function.
  437. // Store the string pointer in the output.
  438. rpszDestOut = sszCurrentString.PRelease();
  439. } // if: there were no errors in this function
  440. else
  441. {
  442. rpszDestOut = NULL;
  443. } // else: something went wrong
  444. RETURN( dwError );
  445. } //*** DwLoadString()
  446. /////////////////////////////////////////////////////////////////////////////
  447. //++
  448. //
  449. // DWORD
  450. // DwWriteOSVersionInfo()
  451. //
  452. // Description:
  453. // This function writes the OS major and minor version information into the
  454. // registry. This information will be used later by ClusOCM to determine the
  455. // OS version before the upgrade.
  456. //
  457. // Arguments:
  458. // const OSVERSIONINFO & rcosviOSVersionInfoIn
  459. // Reference to the OSVERSIONINFO structure that has information about the
  460. // OS version of this node.
  461. //
  462. // Return Value:
  463. // ERROR_SUCCESS if all went well.
  464. // Other Win32 error codes on failure.
  465. //
  466. //--
  467. /////////////////////////////////////////////////////////////////////////////
  468. DWORD
  469. DwWriteOSVersionInfo( const OSVERSIONINFO & rcosviOSVersionInfoIn )
  470. {
  471. TraceFunc( "" );
  472. DWORD dwError = ERROR_SUCCESS;
  473. // dummy do-while loop to avoid gotos
  474. do
  475. {
  476. // Instantiate the SmartRegistryKey smart handle class.
  477. typedef CSmartResource< CHandleTrait< HKEY, LONG, RegCloseKey, NULL > > SmartRegistryKey;
  478. SmartRegistryKey srkOSInfoKey;
  479. {
  480. HKEY hTempKey = NULL;
  481. // Open the node version info registry key
  482. dwError = TW32(
  483. RegCreateKeyEx(
  484. HKEY_LOCAL_MACHINE
  485. , CLUSREG_KEYNAME_NODE_DATA L"\\" CLUSREG_KEYNAME_PREV_OS_INFO
  486. , 0
  487. , L""
  488. , REG_OPTION_NON_VOLATILE
  489. , KEY_ALL_ACCESS
  490. , NULL
  491. , &hTempKey
  492. , NULL
  493. )
  494. );
  495. if ( dwError != ERROR_SUCCESS )
  496. {
  497. TraceFlow1( "Error %#x occurred trying create the registry key where the node OS info is stored.", dwError );
  498. LogMsg( "Error %#x occurred trying create the registry key where the node OS info is stored.", dwError );
  499. break;
  500. } // if: RegCreateKeyEx() failed
  501. srkOSInfoKey.Assign( hTempKey );
  502. }
  503. // Write the OS major version
  504. dwError = TW32(
  505. RegSetValueEx(
  506. srkOSInfoKey.HHandle()
  507. , CLUSREG_NAME_NODE_MAJOR_VERSION
  508. , 0
  509. , REG_DWORD
  510. , reinterpret_cast< const BYTE * >( &rcosviOSVersionInfoIn.dwMajorVersion )
  511. , sizeof( rcosviOSVersionInfoIn.dwMajorVersion )
  512. )
  513. );
  514. if ( dwError != ERROR_SUCCESS )
  515. {
  516. TraceFlow1( "Error %#x occurred trying to store the OS major version info.", dwError );
  517. LogMsg( "Error %#x occurred trying to store the OS major version info.", dwError );
  518. break;
  519. } // if: RegSetValueEx() failed while writing rcosviOSVersionInfoIn.dwMajorVersion
  520. // Write the OS minor version
  521. dwError = TW32(
  522. RegSetValueEx(
  523. srkOSInfoKey.HHandle()
  524. , CLUSREG_NAME_NODE_MINOR_VERSION
  525. , 0
  526. , REG_DWORD
  527. , reinterpret_cast< const BYTE * >( &rcosviOSVersionInfoIn.dwMinorVersion )
  528. , sizeof( rcosviOSVersionInfoIn.dwMinorVersion )
  529. )
  530. );
  531. if ( dwError != ERROR_SUCCESS )
  532. {
  533. TraceFlow1( "Error %#x occurred trying to store the OS minor version info.", dwError );
  534. LogMsg( "Error %#x occurred trying to store the OS minor version info.", dwError );
  535. break;
  536. } // if: RegSetValueEx() failed while writing rcosviOSVersionInfoIn.dwMinorVersion
  537. TraceFlow( "OS version information successfully stored in the registry." );
  538. LogMsg( "OS version information successfully stored in the registry." );
  539. }
  540. while ( false ); // dummy do-while loop to avoid gotos
  541. RETURN( dwError );
  542. } //*** DwWriteOSVersionInfo()