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.

726 lines
21 KiB

  1. /*++
  2. Copyright (c) 1996 Microsoft Corporation
  3. All rights reserved
  4. Abstract:
  5. This module provides functionality for ADs within spooler
  6. Author:
  7. Steve Wilson (NT) July 1997
  8. Revision History:
  9. --*/
  10. #include <precomp.h>
  11. #pragma hdrstop
  12. #include "ds.hxx"
  13. #include "dsprune.hxx"
  14. RETRYLIST gRetry = {NULL, 0, NULL};
  15. #define IADSPATH 0
  16. #define IUNCNAME 1
  17. #define ICN 2
  18. #define IVERSION 3
  19. #define ISERVER 4
  20. #define IFLAGS 5
  21. #define ADSPATH L"ADsPath"
  22. #define CN L"CN"
  23. PWSTR gpszAttributes[] = {ADSPATH, SPLDS_UNC_NAME, CN, SPLDS_VERSION_NUMBER, SPLDS_SERVER_NAME, SPLDS_FLAGS};
  24. #define N_ATTRIBUTES COUNTOF(gpszAttributes)
  25. #define SZ_NO_CACHE L",NoCache"
  26. #define LOG_EVENT_ERROR_BUFFER_SIZE 11
  27. DWORD
  28. SpawnDsPrune(
  29. DWORD dwDelay
  30. )
  31. {
  32. DWORD dwError = ERROR_SUCCESS;
  33. DWORD ThreadId;
  34. HANDLE hDsPruneThread;
  35. PDWORD pdwDelay;
  36. if (pdwDelay = (PDWORD) AllocSplMem(sizeof(DWORD)))
  37. *pdwDelay = dwDelay;
  38. if(!(hDsPruneThread = CreateThread(NULL,
  39. 0,
  40. (LPTHREAD_START_ROUTINE) DsPrune,
  41. (PVOID) pdwDelay,
  42. 0,
  43. &ThreadId))) {
  44. dwError = GetLastError();
  45. FreeSplMem(pdwDelay);
  46. } else {
  47. CloseHandle(hDsPruneThread);
  48. dwError = ERROR_SUCCESS;
  49. }
  50. return dwError;
  51. }
  52. DWORD
  53. WINAPI
  54. DsPrune(
  55. PDWORD pdwDelay
  56. )
  57. {
  58. PWSTR *ppszMySites = NULL;
  59. PWSTR pszDomainRoot = NULL;
  60. ULONG cMySites;
  61. DWORD dwRet;
  62. HRESULT hr;
  63. /*
  64. 1) Determine my site
  65. 2) Query for all PrintQueue objects in my domain
  66. 3) For each PrintQueue:
  67. a) Get array of IP addresses
  68. b) Pass IP addresses to DsAddressToSiteNames
  69. c) If no site returned by DsAddressToSiteNames matches my site, goto 3
  70. d) Delete PrintQueue if orphaned
  71. */
  72. // Sleep random amount on startup so DCs aren't all pruning at the same time
  73. if (pdwDelay) {
  74. Sleep(*pdwDelay*ONE_MINUTE); // *pdwDelay is number of minutes
  75. FreeSplMem(pdwDelay);
  76. }
  77. hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
  78. if (FAILED(hr)) {
  79. return HRESULT_CODE(hr);
  80. }
  81. // Determine my sites
  82. dwRet = DsGetDcSiteCoverage(NULL, &cMySites, &ppszMySites);
  83. if (dwRet != NO_ERROR)
  84. goto error;
  85. dwRet = GetDomainRoot(&pszDomainRoot);
  86. if (dwRet != NERR_Success)
  87. goto error;
  88. while(1) {
  89. PRUNINGPOLICIES PruningPolicies;
  90. DWORD dwSleep, dwOriginalSleep;
  91. PruningPolicies.dwPruneDownlevel = PruneDownlevel();
  92. PruningPolicies.dwPruningRetries = PruningRetries();
  93. PruningPolicies.dwPruningRetryLog = PruningRetryLog();
  94. SetPruningPriority();
  95. DeleteOrphans(ppszMySites, cMySites, pszDomainRoot, &PruningPolicies);
  96. dwSleep = dwOriginalSleep = PruningInterval();
  97. while (dwSleep > HOUR_OF_MINUTES) {
  98. Sleep(ONE_HOUR); // Check interval every hour
  99. DWORD dwNewSleep = PruningInterval();
  100. if (dwNewSleep != dwOriginalSleep) {
  101. dwSleep = dwOriginalSleep = dwNewSleep;
  102. }
  103. if (dwSleep != INFINITE && dwSleep > HOUR_OF_MINUTES)
  104. dwSleep -= HOUR_OF_MINUTES;
  105. }
  106. Sleep(dwSleep*ONE_MINUTE);
  107. }
  108. error:
  109. if (ppszMySites)
  110. NetApiBufferFree(ppszMySites);
  111. FreeSplMem(pszDomainRoot);
  112. CoUninitialize();
  113. return ERROR_SUCCESS;
  114. }
  115. HRESULT
  116. DeleteOrphans(
  117. PWSTR *ppszMySites,
  118. ULONG cMySites,
  119. PWSTR pszSearchRoot,
  120. PRUNINGPOLICIES *pPruningPolicies
  121. )
  122. {
  123. ADS_SEARCH_HANDLE hSearchHandle = NULL;
  124. IDirectorySearch *pDSSearch = NULL;
  125. HRESULT hr = S_OK;
  126. WCHAR szSearchPattern[] = L"(objectCategory=printQueue)";
  127. SEARCHCOLUMN Col[N_ATTRIBUTES];
  128. DWORD i;
  129. ADS_SEARCHPREF_INFO SearchPrefs;
  130. DWORD dwError;
  131. // Find all PrintQueues in domain
  132. hr = ADsGetObject( pszSearchRoot,
  133. IID_IDirectorySearch,
  134. (void **)&pDSSearch);
  135. BAIL_ON_FAILURE(hr);
  136. SearchPrefs.dwSearchPref = ADS_SEARCHPREF_PAGESIZE;
  137. SearchPrefs.vValue.dwType = ADSTYPE_INTEGER;
  138. SearchPrefs.vValue.Integer = 256;
  139. hr = pDSSearch->SetSearchPreference(&SearchPrefs, 1);
  140. if (FAILED(hr)) {
  141. DBGMSG(DBG_WARNING, ("DSPrune: SetSearchPref failed: %d\n", hr));
  142. } else if (hr != S_OK && SearchPrefs.dwStatus != ADS_STATUS_S_OK) {
  143. DBGMSG(DBG_WARNING, ("DSPrune: SearchPref failed: %d\n", SearchPrefs.dwStatus));
  144. }
  145. hr = pDSSearch->ExecuteSearch(
  146. szSearchPattern,
  147. gpszAttributes,
  148. N_ATTRIBUTES,
  149. &hSearchHandle);
  150. BAIL_ON_FAILURE(hr);
  151. hr = pDSSearch->GetNextRow(hSearchHandle);
  152. //
  153. // This is the new way of checking for rows in Whistler.
  154. // see bug 163776. I expect to be changed in the future.
  155. //
  156. if (hr == S_ADS_NOMORE_ROWS) {
  157. ADsGetLastError(&dwError, NULL, 0, NULL, 0);
  158. if (dwError == ERROR_MORE_DATA) {
  159. hr = pDSSearch->GetNextRow(hSearchHandle);
  160. }
  161. }
  162. BAIL_ON_FAILURE(hr);
  163. while (hr != S_ADS_NOMORE_ROWS) {
  164. for (i = 0 ; i < N_ATTRIBUTES ; ++i)
  165. Col[i].hr = pDSSearch->GetColumn(hSearchHandle, gpszAttributes[i], &Col[i].Column);
  166. DeleteOrphan(ppszMySites, cMySites, Col, pPruningPolicies);
  167. for (i = 0 ; i < N_ATTRIBUTES ; ++i)
  168. if (SUCCEEDED(Col[i].hr))
  169. pDSSearch->FreeColumn(&Col[i].Column);
  170. hr = pDSSearch->GetNextRow(hSearchHandle);
  171. //
  172. // This is the new way of checking for rows in Whistler.
  173. // see bug 163776. I expect to be changed in the future.
  174. //
  175. if (hr == S_ADS_NOMORE_ROWS) {
  176. ADsGetLastError(&dwError, NULL, 0, NULL, 0);
  177. if (dwError == ERROR_MORE_DATA) {
  178. hr = pDSSearch->GetNextRow(hSearchHandle);
  179. }
  180. }
  181. BAIL_ON_FAILURE(hr);
  182. }
  183. hr = S_OK;
  184. error:
  185. if (hSearchHandle)
  186. pDSSearch->CloseSearchHandle(hSearchHandle);
  187. if (pDSSearch)
  188. pDSSearch->Release();
  189. return hr;
  190. }
  191. VOID
  192. DeleteOrphan(
  193. PWSTR *ppszMySites,
  194. ULONG cMySites,
  195. SEARCHCOLUMN Col[],
  196. PRUNINGPOLICIES *pPruningPolicies
  197. )
  198. {
  199. IADs *pADs = NULL;
  200. IADsContainer *pContainer = NULL;
  201. PWSTR pszParent = NULL;
  202. PWSTR pszCommonName = NULL;
  203. HANDLE hPrinter = NULL;
  204. PWSTR pszServerName = NULL;
  205. PWSTR pszEscapedCN = NULL;
  206. PWSTR pszCN;
  207. PWSTR pszADsPath;
  208. PWSTR pszUNCName;
  209. PWSTR pszServer;
  210. DWORD dwError = ERROR_SUCCESS;
  211. HRESULT hr;
  212. DWORD dwVersion;
  213. DWORD dwFlags;
  214. DWORD cb;
  215. BOOL bDeleteIt = FALSE;
  216. BOOL bDeleteImmediately = FALSE;
  217. WCHAR ErrorBuffer[LOG_EVENT_ERROR_BUFFER_SIZE];
  218. pszADsPath = SUCCEEDED(Col[IADSPATH].hr) ? Col[IADSPATH].Column.pADsValues->DNString : NULL;
  219. pszUNCName = SUCCEEDED(Col[IUNCNAME].hr) ? Col[IUNCNAME].Column.pADsValues->DNString : NULL;
  220. pszServer = SUCCEEDED(Col[ISERVER].hr) ? Col[ISERVER].Column.pADsValues->DNString : NULL;
  221. pszCN = SUCCEEDED(Col[ICN].hr) ? Col[ICN].Column.pADsValues->DNString : NULL;
  222. dwVersion = SUCCEEDED(Col[IVERSION].hr) ? Col[IVERSION].Column.pADsValues->Integer : 0;
  223. dwFlags = SUCCEEDED(Col[IFLAGS].hr) ? Col[IFLAGS].Column.pADsValues->Integer : 0;
  224. // We should always have an ADsPath & CN, but if not, don't continue because there's
  225. // no way to delete something if we don't know where it is.
  226. if (!pszADsPath || !pszCN) {
  227. DBGMSG(DBG_WARNING, ("DeleteOrphan: No ADs Path or CN!\n"));
  228. return;
  229. }
  230. if (!(dwFlags & IMMORTAL)) {
  231. if (!pszUNCName || !pszServer || FAILED(Col[IVERSION].hr)) {
  232. bDeleteIt = bDeleteImmediately = TRUE;
  233. SplLogEvent( pLocalIniSpooler,
  234. LOG_INFO,
  235. MSG_PRUNING_NOUNC_PRINTER,
  236. FALSE,
  237. pszADsPath,
  238. NULL );
  239. } else if ((dwVersion >= DS_PRINTQUEUE_VERSION_WIN2000 ||
  240. pPruningPolicies->dwPruneDownlevel != PRUNE_DOWNLEVEL_NEVER) &&
  241. UNC2Server(pszUNCName, &pszServerName) == ERROR_SUCCESS &&
  242. ServerOnSite(ppszMySites, cMySites, pszServerName)) {
  243. // Try to open the printer. If it doesn't exist, delete it!
  244. PWSTR pszNoCacheUNCName = NULL;
  245. cb = (wcslen(pszUNCName) + 1)*sizeof *pszUNCName;
  246. cb += sizeof(SZ_NO_CACHE);
  247. pszNoCacheUNCName = (PWSTR) AllocSplMem(cb);
  248. if (pszNoCacheUNCName) { // If Alloc fails, just go on to the next printer
  249. wcscpy(pszNoCacheUNCName, pszUNCName);
  250. wcscat(pszNoCacheUNCName, SZ_NO_CACHE);
  251. DBGMSG(DBG_EXEC, ("DSPrune: Checking %ws\n", pszUNCName));
  252. if (!OpenPrinter(pszNoCacheUNCName, &hPrinter, NULL)) {
  253. dwError = GetLastError();
  254. if (dwError != ERROR_ACCESS_DENIED &&
  255. (dwVersion >= DS_PRINTQUEUE_VERSION_WIN2000 ||
  256. pPruningPolicies->dwPruneDownlevel == PRUNE_DOWNLEVEL_AGGRESSIVELY ||
  257. (pPruningPolicies->dwPruneDownlevel == PRUNE_DOWNLEVEL_NICELY && ServerExists(pszServerName)))) {
  258. bDeleteIt = TRUE;
  259. //
  260. // Log an info event for each retry, if the policy is configured so.
  261. //
  262. if (pPruningPolicies->dwPruningRetryLog) {
  263. wsprintf(ErrorBuffer, L"%0x", dwError);
  264. SplLogEvent( pLocalIniSpooler,
  265. LOG_INFO,
  266. MSG_PRUNING_ABSENT_PRINTER,
  267. FALSE,
  268. pszADsPath,
  269. ErrorBuffer,
  270. NULL );
  271. }
  272. }
  273. } else if (dwVersion >= DS_PRINTQUEUE_VERSION_WIN2000) {
  274. BYTE Data[256];
  275. DWORD cbNeeded = 0;
  276. BYTE *pData = Data;
  277. BYTE bHaveData = TRUE;
  278. // Verify that the printer should indeed be published and that
  279. // the GUID of the published object matches the GUID on the print server.
  280. // NOTE: The GUID check is needed because we may publish the same printer
  281. // twice if the spooler is restarted and checks a DC before the first publish
  282. // is replicated.
  283. if (!GetPrinter(hPrinter, 7, Data, COUNTOF(Data), &cbNeeded)) {
  284. if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
  285. // Forget this printer if alloc or second GetPrinter fails
  286. pData = (PBYTE) AllocSplMem(cbNeeded);
  287. if (pData) {
  288. if (!GetPrinter(hPrinter, 7, pData, cbNeeded, &cbNeeded)) {
  289. bHaveData = FALSE;
  290. }
  291. } else {
  292. bHaveData = FALSE;
  293. }
  294. } else {
  295. //
  296. // A Win9x machine may have the same printer UNCName
  297. //
  298. bDeleteIt = TRUE;
  299. bHaveData = FALSE;
  300. }
  301. }
  302. if (bHaveData) {
  303. if (!(((PPRINTER_INFO_7) pData)->dwAction & DSPRINT_PUBLISH)) {
  304. bDeleteIt = bDeleteImmediately = TRUE;
  305. SplLogEvent( pLocalIniSpooler,
  306. LOG_INFO,
  307. MSG_PRUNING_UNPUBLISHED_PRINTER,
  308. FALSE,
  309. pszADsPath,
  310. NULL );
  311. } else {
  312. // Compare object GUID to printer's GUID
  313. PWSTR pszObjectGUID = NULL;
  314. hr = ADsGetObject(pszADsPath, IID_IADs, (void **) &pADs);
  315. BAIL_ON_FAILURE(hr);
  316. hr = GetGUID(pADs, &pszObjectGUID);
  317. BAIL_ON_FAILURE(hr);
  318. if (!((PPRINTER_INFO_7) pData)->pszObjectGUID ||
  319. wcscmp(((PPRINTER_INFO_7) pData)->pszObjectGUID, pszObjectGUID)) {
  320. bDeleteIt = bDeleteImmediately = TRUE;
  321. SplLogEvent( pLocalIniSpooler,
  322. LOG_INFO,
  323. MSG_PRUNING_DUPLICATE_PRINTER,
  324. FALSE,
  325. pszADsPath,
  326. NULL );
  327. }
  328. FreeSplStr(pszObjectGUID);
  329. pADs->Release();
  330. pADs = NULL;
  331. }
  332. }
  333. if (pData != Data)
  334. FreeSplMem(pData);
  335. }
  336. FreeSplMem(pszNoCacheUNCName);
  337. }
  338. }
  339. }
  340. // Get PrintQueue object
  341. hr = ADsGetObject(pszADsPath, IID_IADs, (void **) &pADs);
  342. BAIL_ON_FAILURE(hr);
  343. if (bDeleteIt) {
  344. // Delete the printqueue
  345. // Check retry count
  346. if (!bDeleteImmediately && !EnoughRetries(pADs, pPruningPolicies->dwPruningRetries))
  347. goto error;
  348. // Get the Parent ADsPath
  349. hr = pADs->get_Parent(&pszParent);
  350. BAIL_ON_FAILURE(hr);
  351. // Get the Parent object
  352. hr = ADsGetObject( pszParent,
  353. IID_IADsContainer,
  354. (void **) &pContainer);
  355. BAIL_ON_FAILURE(hr);
  356. // The CN string read from ExecuteSearch may have unescaped characters, so
  357. // be sure to fix this before trying to delete
  358. pszEscapedCN = CreateEscapedString(pszCN, DN_SPECIAL_CHARS);
  359. if (!pszEscapedCN) {
  360. hr = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_WIN32, GetLastError());
  361. BAIL_ON_FAILURE(hr);
  362. }
  363. // pszCN needs to be formatted as: "CN=Name"
  364. cb = (wcslen(pszEscapedCN) + 4)*sizeof(WCHAR);
  365. if (!(pszCommonName = (PWSTR) AllocSplMem(cb))) {
  366. hr = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_WIN32, GetLastError());
  367. BAIL_ON_FAILURE(hr);
  368. }
  369. wcscpy(pszCommonName, L"CN=");
  370. wcscat(pszCommonName, pszEscapedCN);
  371. hr = pContainer->Delete(SPLDS_PRINTER_CLASS, pszCommonName);
  372. if (FAILED(hr)) {
  373. wsprintf(ErrorBuffer, L"%0x", hr);
  374. SplLogEvent( pLocalIniSpooler,
  375. LOG_ERROR,
  376. MSG_CANT_PRUNE_PRINTER,
  377. FALSE,
  378. pszADsPath,
  379. ErrorBuffer,
  380. NULL );
  381. DBGMSG(DBG_EXEC, ("DSPrune: Can't delete %ws. Error: %0x\n", pszUNCName, hr));
  382. goto error;
  383. }
  384. SplLogEvent( pLocalIniSpooler,
  385. LOG_INFO,
  386. MSG_PRUNING_PRINTER,
  387. FALSE,
  388. pszADsPath,
  389. NULL );
  390. DBGMSG(DBG_EXEC, ("DSPrune: DELETING %ws\n", pszUNCName));
  391. } else {
  392. // Delete the Retry Entry (if any) if we aren't deleting it
  393. DeleteRetryEntry(pADs);
  394. }
  395. error:
  396. if (hPrinter)
  397. ClosePrinter(hPrinter);
  398. if (pADs)
  399. pADs->Release();
  400. if (pContainer)
  401. pContainer->Release();
  402. if (pszParent)
  403. SysFreeString(pszParent);
  404. FreeSplMem(pszCommonName);
  405. FreeSplMem(pszEscapedCN);
  406. FreeSplMem(pszServerName);
  407. }
  408. BOOL
  409. EnoughRetries(
  410. IADs *pADs,
  411. DWORD dwPruningRetries
  412. )
  413. /*++
  414. Function Description:
  415. This function return TRUE if we have retry count is greater than retry policy, FALSE otherwise
  416. Parameters:
  417. pADs - pointer to the PrintQueue object
  418. Return Values:
  419. TRUE if retry count is greater than the policy setting for this object
  420. FALSE if we can't get the GUID or entry doesn't exist and it can't be created (out of memory)
  421. --*/
  422. {
  423. HRESULT hr = S_OK;
  424. PWSTR pszObjectGUID = NULL;
  425. PRETRYLIST pRetry = NULL;
  426. BOOL bEnoughRetries = FALSE;
  427. // Get the GUID
  428. hr = GetGUID(pADs, &pszObjectGUID);
  429. BAIL_ON_FAILURE(hr);
  430. // Get the entry
  431. if (!(pRetry = GetRetry(pszObjectGUID))) {
  432. hr = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_WIN32, GetLastError());
  433. BAIL_ON_FAILURE(hr);
  434. }
  435. // Increment retry count if we're not done. Delete retry if we're done.
  436. if (++pRetry->nRetries > dwPruningRetries) {
  437. DeleteRetry(pRetry);
  438. bEnoughRetries = TRUE;
  439. }
  440. error:
  441. FreeSplStr(pszObjectGUID);
  442. return bEnoughRetries;
  443. }
  444. VOID
  445. DeleteRetryEntry(
  446. IADs *pADs
  447. )
  448. /*++
  449. Function Description:
  450. This function removes a Retry entry based on the supplied ADs pointer
  451. Parameters:
  452. pADs - pointer to the PrintQueue object
  453. Return Values:
  454. none
  455. --*/
  456. {
  457. HRESULT hr = S_OK;
  458. PWSTR pszObjectGUID = NULL;
  459. PRETRYLIST pRetry = NULL;
  460. // Get the GUID
  461. hr = GetGUID(pADs, &pszObjectGUID);
  462. BAIL_ON_FAILURE(hr);
  463. // Get & Delete the entry
  464. if (pRetry = FindRetry(pszObjectGUID)) {
  465. DeleteRetry(pRetry);
  466. }
  467. error:
  468. FreeSplStr(pszObjectGUID);
  469. }
  470. PRETRYLIST
  471. FindRetry(
  472. PWSTR pszObjectGUID
  473. )
  474. /*++
  475. Function Description:
  476. This function finds a RETRYLIST entry.
  477. Parameters:
  478. pszObjectGUID - pointer to buffer containing the GUID of the RETRYLIST entry to find or create.
  479. Return Values:
  480. PRETRYLIST - pointer to the found or create RETRYLIST entry. This may be NULL if the entry is
  481. not found and a new one could not be created.
  482. --*/
  483. {
  484. PRETRYLIST pRetry = &gRetry;
  485. for (; pRetry->pNext ; pRetry = pRetry->pNext) {
  486. // If entry exists, just return
  487. if (!wcscmp(pszObjectGUID, pRetry->pNext->pszObjectGUID))
  488. return pRetry->pNext;
  489. }
  490. return NULL;
  491. }
  492. PRETRYLIST
  493. GetRetry(
  494. PWSTR pszObjectGUID
  495. )
  496. /*++
  497. Function Description:
  498. This function finds or creates a RETRYLIST entry.
  499. Parameters:
  500. pszObjectGUID - pointer to buffer containing the GUID of the RETRYLIST entry to find or create.
  501. Return Values:
  502. PRETRYLIST - pointer to the found or create RETRYLIST entry. This may be NULL if the entry is
  503. not found and a new one could not be created.
  504. --*/
  505. {
  506. PRETRYLIST pRetry = &gRetry;
  507. int iRet = -1;
  508. for (; pRetry->pNext ; pRetry = pRetry->pNext) {
  509. // If entry exists, just return
  510. if (!(iRet = wcscmp(pszObjectGUID, pRetry->pNext->pszObjectGUID)))
  511. return pRetry->pNext;
  512. // If next entry is greater than New entry, then insert New entry here
  513. if (iRet > 0)
  514. break;
  515. }
  516. // Create a new entry
  517. PRETRYLIST pRetryNew;
  518. if (!(pRetryNew = (PRETRYLIST) AllocSplMem(sizeof(RETRYLIST))))
  519. return NULL;
  520. if (!(pRetryNew->pszObjectGUID = (PWSTR) AllocSplStr(pszObjectGUID))) {
  521. FreeSplMem(pRetryNew);
  522. return NULL;
  523. }
  524. if (!pRetry->pNext) { // End of list
  525. pRetryNew->pNext = NULL;
  526. pRetryNew->pPrev = pRetry;
  527. pRetry->pNext = pRetryNew;
  528. } else { // Middle of list
  529. pRetryNew->pNext = pRetry->pNext;
  530. pRetryNew->pPrev = pRetry;
  531. pRetry->pNext->pPrev = pRetryNew;
  532. pRetry->pNext = pRetryNew;
  533. }
  534. pRetryNew->nRetries = 0;
  535. return pRetryNew;
  536. }
  537. VOID
  538. DeleteRetry(
  539. PRETRYLIST pRetry
  540. )
  541. {
  542. SPLASSERT(pRetry != &gRetry);
  543. SPLASSERT(pRetry);
  544. SPLASSERT(pRetry->pszObjectGUID);
  545. SPLASSERT(pRetry->pPrev);
  546. pRetry->pPrev->pNext = pRetry->pNext;
  547. if (pRetry->pNext)
  548. pRetry->pNext->pPrev = pRetry->pPrev;
  549. FreeSplStr(pRetry->pszObjectGUID);
  550. FreeSplMem(pRetry);
  551. }