Leaked source code of windows server 2003
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.

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