697 lines
15 KiB
C
697 lines
15 KiB
C
|
/*++
|
||
|
|
||
|
Copyright (c) 1990-1994 Microsoft Corporation
|
||
|
All rights reserved
|
||
|
|
||
|
Module Name:
|
||
|
|
||
|
threads.c (thread manager)
|
||
|
|
||
|
Abstract:
|
||
|
|
||
|
Handles the threads used to for notifications (WPC, FFPCN)
|
||
|
|
||
|
Author:
|
||
|
|
||
|
Albert Ting (AlbertT) 25-Jan-94
|
||
|
|
||
|
Environment:
|
||
|
|
||
|
User Mode -Win32
|
||
|
|
||
|
Revision History:
|
||
|
|
||
|
--*/
|
||
|
|
||
|
#include "precomp.h"
|
||
|
#pragma hdrstop
|
||
|
|
||
|
#include "threadm.h"
|
||
|
#include "ntfytab.h"
|
||
|
|
||
|
#define ENTER_THREAD_LIST() EnterCriticalSection(tmStateStatic.pCritSec)
|
||
|
#define EXIT_THREAD_LIST() LeaveCriticalSection(tmStateStatic.pCritSec)
|
||
|
|
||
|
extern CRITICAL_SECTION RouterNotifySection;
|
||
|
|
||
|
DWORD
|
||
|
ThreadNotifyProcessJob(
|
||
|
PTMSTATEVAR pTMStateVar,
|
||
|
PJOB pJob);
|
||
|
|
||
|
PJOB
|
||
|
ThreadNotifyNextJob(
|
||
|
PTMSTATEVAR ptmStateVar);
|
||
|
|
||
|
|
||
|
TMSTATESTATIC tmStateStatic = {
|
||
|
10,
|
||
|
2500,
|
||
|
(PFNPROCESSJOB)ThreadNotifyProcessJob,
|
||
|
(PFNNEXTJOB)ThreadNotifyNextJob,
|
||
|
NULL,
|
||
|
NULL,
|
||
|
&RouterNotifySection
|
||
|
};
|
||
|
|
||
|
TMSTATEVAR tmStateVar;
|
||
|
PCHANGE pChangeList;
|
||
|
|
||
|
|
||
|
WCHAR szThreadMax[] = L"ThreadNotifyMax";
|
||
|
WCHAR szThreadIdleLife[] = L"ThreadNotifyIdleLife";
|
||
|
WCHAR szThreadNotifySleep[] = L"ThreadNotifySleep";
|
||
|
|
||
|
DWORD dwThreadNotifySleep = 1000;
|
||
|
|
||
|
|
||
|
BOOL
|
||
|
ThreadInit()
|
||
|
{
|
||
|
HKEY hKey;
|
||
|
DWORD dwType = REG_DWORD;
|
||
|
DWORD cbData;
|
||
|
|
||
|
if (!TMCreateStatic(&tmStateStatic))
|
||
|
return FALSE;
|
||
|
|
||
|
if (!TMCreate(&tmStateStatic, &tmStateVar))
|
||
|
return FALSE;
|
||
|
|
||
|
if (!RegOpenKeyEx(HKEY_LOCAL_MACHINE,
|
||
|
szPrintKey,
|
||
|
0,
|
||
|
KEY_READ,
|
||
|
&hKey)) {
|
||
|
|
||
|
cbData = sizeof(tmStateStatic.uMaxThreads);
|
||
|
|
||
|
//
|
||
|
// Ignore failure case since we default to 10.
|
||
|
//
|
||
|
RegQueryValueEx(hKey,
|
||
|
szThreadMax,
|
||
|
NULL,
|
||
|
&dwType,
|
||
|
(LPBYTE)&tmStateStatic.uMaxThreads,
|
||
|
&cbData);
|
||
|
|
||
|
cbData = sizeof(tmStateStatic.uIdleLife);
|
||
|
|
||
|
//
|
||
|
// Ignore failure case since we default to 1000 (1 sec).
|
||
|
//
|
||
|
RegQueryValueEx(hKey,
|
||
|
szThreadIdleLife,
|
||
|
NULL,
|
||
|
&dwType,
|
||
|
(LPBYTE)&tmStateStatic.uIdleLife,
|
||
|
&cbData);
|
||
|
|
||
|
cbData = sizeof(dwThreadNotifySleep);
|
||
|
|
||
|
//
|
||
|
// Ignore failure case since we default to 2500 (2.5 sec).
|
||
|
//
|
||
|
RegQueryValueEx(hKey,
|
||
|
szThreadNotifySleep,
|
||
|
NULL,
|
||
|
&dwType,
|
||
|
(LPBYTE)&dwThreadNotifySleep,
|
||
|
&cbData);
|
||
|
|
||
|
|
||
|
RegCloseKey(hKey);
|
||
|
}
|
||
|
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
VOID
|
||
|
ThreadDestroy()
|
||
|
{
|
||
|
TMDestroy(&tmStateVar);
|
||
|
TMDestroyStatic(&tmStateStatic);
|
||
|
}
|
||
|
|
||
|
|
||
|
BOOL
|
||
|
LinkChange(
|
||
|
PCHANGE pChange)
|
||
|
|
||
|
/*++
|
||
|
|
||
|
Routine Description:
|
||
|
|
||
|
Link up the change to the list of jobs that need to be processed.
|
||
|
If the call succeeded but there was an overflow at the client,
|
||
|
then we won't add this to the list until it gets refreshed.
|
||
|
|
||
|
Arguments:
|
||
|
|
||
|
Return Value:
|
||
|
|
||
|
--*/
|
||
|
|
||
|
{
|
||
|
if (pChange->eStatus & STATUS_CHANGE_DISCARDNOTED) {
|
||
|
|
||
|
return FALSE;
|
||
|
}
|
||
|
pChange->cRef++;
|
||
|
pChange->eStatus |= STATUS_CHANGE_ACTIVE;
|
||
|
LinkAdd(&pChange->Link, (PLINK*)&pChangeList);
|
||
|
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
|
||
|
BOOL
|
||
|
ThreadNotify(
|
||
|
LPPRINTHANDLE pPrintHandle)
|
||
|
|
||
|
/*++
|
||
|
|
||
|
Routine Description:
|
||
|
|
||
|
Handles notifying the remote clients of changes.
|
||
|
|
||
|
Arguments:
|
||
|
|
||
|
pPrintHandle - printer that requires notification
|
||
|
|
||
|
Return Value:
|
||
|
|
||
|
TRUE = success, GetLastError() valid on FALSE.
|
||
|
|
||
|
NOTE: Currenly only supports grouping
|
||
|
|
||
|
--*/
|
||
|
|
||
|
{
|
||
|
PCHANGE pChange = pPrintHandle->pChange;
|
||
|
|
||
|
ENTER_THREAD_LIST();
|
||
|
|
||
|
//
|
||
|
// Only add if we're not on the linked list already.
|
||
|
//
|
||
|
if (!(pChange->eStatus & STATUS_CHANGE_ACTIVE)) {
|
||
|
|
||
|
DBGMSG(DBG_NOTIFY, ("TMN: link added 0x%x cRef++ %d\n",
|
||
|
pChange,
|
||
|
pChange->cRef));
|
||
|
|
||
|
//
|
||
|
// Only add ourseleves to the linked list and
|
||
|
// Notify via TMAddJob if we are not on the list.
|
||
|
//
|
||
|
if (LinkChange(pChange))
|
||
|
TMAddJob(&tmStateVar);
|
||
|
|
||
|
} else {
|
||
|
|
||
|
pChange->eStatus |= STATUS_CHANGE_ACTIVE_REQ;
|
||
|
DBGMSG(DBG_NOTIFY, ("TMN: In LL already 0x%x cRef %d\n",
|
||
|
pChange,
|
||
|
pChange->cRef));
|
||
|
}
|
||
|
|
||
|
|
||
|
EXIT_THREAD_LIST();
|
||
|
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
|
||
|
PJOB
|
||
|
ThreadNotifyNextJob(
|
||
|
PTMSTATEVAR ptmStateVar)
|
||
|
|
||
|
/*++
|
||
|
|
||
|
Routine Description:
|
||
|
|
||
|
Callback to get the next job.
|
||
|
|
||
|
Arguments:
|
||
|
|
||
|
ptmStateVar - ignored.
|
||
|
|
||
|
Return Value:
|
||
|
|
||
|
pJob (pChange)
|
||
|
|
||
|
--*/
|
||
|
|
||
|
{
|
||
|
PCHANGE pChange;
|
||
|
|
||
|
ENTER_THREAD_LIST();
|
||
|
|
||
|
//
|
||
|
// If there are no jobs left, quit.
|
||
|
//
|
||
|
pChange = (PCHANGE)pChangeList;
|
||
|
|
||
|
DBGMSG(DBG_NOTIFY, ("ThreadNotifyNextJob: Removing pChange 0x%x\n",
|
||
|
pChange));
|
||
|
|
||
|
if (pChange) {
|
||
|
LinkDelete(&pChange->Link, (PLINK*)&pChangeList);
|
||
|
}
|
||
|
|
||
|
EXIT_THREAD_LIST();
|
||
|
|
||
|
return (PJOB)pChange;
|
||
|
}
|
||
|
|
||
|
DWORD
|
||
|
ThreadNotifyProcessJob(
|
||
|
PTMSTATEVAR pTMStateVar,
|
||
|
PJOB pJob)
|
||
|
|
||
|
/*++
|
||
|
|
||
|
Routine Description:
|
||
|
|
||
|
Does the actual RPC call to notify the client.
|
||
|
|
||
|
Arguments:
|
||
|
|
||
|
pJob = pChange structure
|
||
|
|
||
|
Return Value:
|
||
|
|
||
|
--*/
|
||
|
|
||
|
{
|
||
|
PCHANGE pChange = (PCHANGE)pJob;
|
||
|
DWORD fdwChangeFlags;
|
||
|
HANDLE hNotifyRemote;
|
||
|
PPRINTER_NOTIFY_INFO pPrinterNotifyInfo;
|
||
|
PPRINTER_NOTIFY_INFO pPrinterNotifyInfoNew;
|
||
|
RPC_V2_UREPLY_PRINTER Reply;
|
||
|
|
||
|
DWORD dwReturn;
|
||
|
DWORD dwResult = 0;
|
||
|
DWORD dwColor;
|
||
|
|
||
|
ENTER_THREAD_LIST();
|
||
|
|
||
|
if (pChange->eStatus & STATUS_CHANGE_CLOSING) {
|
||
|
|
||
|
//
|
||
|
// Abort this job
|
||
|
//
|
||
|
dwReturn = ERROR_INVALID_PARAMETER;
|
||
|
goto Done;
|
||
|
}
|
||
|
|
||
|
fdwChangeFlags = pChange->fdwChangeFlags;
|
||
|
pChange->fdwChangeFlags = 0;
|
||
|
|
||
|
//
|
||
|
// We must save out this copy in case we have some more info
|
||
|
// while we are RPCing. Turn off LL_REQ because we are about
|
||
|
// to process this batch.
|
||
|
//
|
||
|
pPrinterNotifyInfo = pChange->ChangeInfo.pPrinterNotifyInfo;
|
||
|
|
||
|
#if DBG
|
||
|
if( !pPrinterNotifyInfo && !fdwChangeFlags ){
|
||
|
DBGMSG( DBG_WARN, ( "ThreadNotifyProcessJob: No change information\n" ));
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
pChange->ChangeInfo.pPrinterNotifyInfo = NULL;
|
||
|
pChange->eStatus &= ~STATUS_CHANGE_ACTIVE_REQ;
|
||
|
dwColor = pChange->dwColor;
|
||
|
|
||
|
//
|
||
|
// We were already marked in use when the job was added.
|
||
|
// If another thread wants to delete it, they should OR in
|
||
|
// STATUS_CHANGE_CLOSING, which we will pickup.
|
||
|
//
|
||
|
|
||
|
EXIT_THREAD_LIST();
|
||
|
|
||
|
if (pChange->hNotifyRemote) {
|
||
|
|
||
|
DBGMSG(DBG_NOTIFY, (">> Remoting pChange 0x%x hNotifyRemote 0x%x\n",
|
||
|
pChange,
|
||
|
pChange->hNotifyRemote));
|
||
|
|
||
|
RpcTryExcept {
|
||
|
|
||
|
//
|
||
|
// Note:
|
||
|
//
|
||
|
// We should not be impersonating at this stage since
|
||
|
// we will get a separate session id.
|
||
|
//
|
||
|
if (pPrinterNotifyInfo) {
|
||
|
|
||
|
Reply.pInfo = (PRPC_V2_NOTIFY_INFO)pPrinterNotifyInfo;
|
||
|
|
||
|
//
|
||
|
// Remote case; bind and call the remote router.
|
||
|
//
|
||
|
dwReturn = RpcRouterReplyPrinterEx(
|
||
|
pChange->hNotifyRemote,
|
||
|
dwColor,
|
||
|
fdwChangeFlags,
|
||
|
&dwResult,
|
||
|
REPLY_PRINTER_CHANGE,
|
||
|
Reply);
|
||
|
} else {
|
||
|
|
||
|
dwReturn = RpcRouterReplyPrinter(
|
||
|
pChange->hNotifyRemote,
|
||
|
fdwChangeFlags,
|
||
|
1,
|
||
|
(PBYTE)&pPrinterNotifyInfo);
|
||
|
}
|
||
|
|
||
|
} RpcExcept(I_RpcExceptionFilter(RpcExceptionCode())) {
|
||
|
|
||
|
dwReturn = RpcExceptionCode();
|
||
|
|
||
|
} RpcEndExcept
|
||
|
|
||
|
} else {
|
||
|
|
||
|
dwReturn = ERROR_INVALID_HANDLE;
|
||
|
DBGMSG(DBG_WARNING, ("ThreadNotifyProcessJob: no hNotifyRemote\n"));
|
||
|
}
|
||
|
|
||
|
if (dwReturn) {
|
||
|
|
||
|
DBGMSG(DBG_WARNING, ("ThreadNotifyProcessJob: RPC error %d\npChange 0x%x, hNotifyRemote 0x%x, hPrinterRemote 0x%x\n",
|
||
|
dwReturn,
|
||
|
pChange,
|
||
|
pChange->hNotifyRemote,
|
||
|
pChange->dwPrinterRemote));
|
||
|
|
||
|
//
|
||
|
// On error, close and retry
|
||
|
//
|
||
|
CloseReplyRemote(pChange->hNotifyRemote);
|
||
|
|
||
|
if (OpenReplyRemote(pChange->pszLocalMachine,
|
||
|
&pChange->hNotifyRemote,
|
||
|
pChange->dwPrinterRemote,
|
||
|
REPLY_TYPE_NOTIFICATION,
|
||
|
0,
|
||
|
NULL)) {
|
||
|
|
||
|
pChange->hNotifyRemote = NULL;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Sleep(dwThreadNotifySleep);
|
||
|
|
||
|
ENTER_THREAD_LIST();
|
||
|
|
||
|
pPrinterNotifyInfoNew = pChange->ChangeInfo.pPrinterNotifyInfo;
|
||
|
|
||
|
//
|
||
|
// Keep dwResult only if the color is current.
|
||
|
//
|
||
|
// The avoids the problem when the server sends a discard (RPC1),
|
||
|
// then the client refreshes (RPC2). RPC2 returns first, clearing
|
||
|
// the client's discard bit. The server overflows the new
|
||
|
// buffer. RPC1 completes, and returns discardnoted, which is
|
||
|
// incorrect since it is stale. If the color isn't checked, then
|
||
|
// the server thinks that the discard has been noted when it really
|
||
|
// has not.
|
||
|
//
|
||
|
if (dwColor != pChange->dwColor) {
|
||
|
dwResult = 0;
|
||
|
}
|
||
|
|
||
|
if (pPrinterNotifyInfo) {
|
||
|
|
||
|
//
|
||
|
// Handle different error states from RPC. Each case/default
|
||
|
// block must either update or pChange->ChangeInfo.pPrinterNotifyInfo
|
||
|
// and free the old or the new if both exist.
|
||
|
//
|
||
|
switch (dwReturn) {
|
||
|
case ERROR_SUCCESS:
|
||
|
|
||
|
//
|
||
|
// On success, see if the client saw the info but couldn't
|
||
|
// store it since they overflowed. In this case, we note
|
||
|
// this and never RPC to them again until they refresh.
|
||
|
//
|
||
|
if (dwResult & PRINTER_NOTIFY_INFO_DISCARDNOTED) {
|
||
|
pChange->eStatus |= (STATUS_CHANGE_DISCARDED |
|
||
|
STATUS_CHANGE_DISCARDNOTED);
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// If new buffer allocated, free our old one, else reuse buffer.
|
||
|
//
|
||
|
if (!pPrinterNotifyInfoNew) {
|
||
|
|
||
|
//
|
||
|
// Clear it since we are reusing it.
|
||
|
//
|
||
|
ClearPrinterNotifyInfo(pPrinterNotifyInfo, pChange);
|
||
|
pChange->ChangeInfo.pPrinterNotifyInfo = pPrinterNotifyInfo;
|
||
|
|
||
|
} else {
|
||
|
|
||
|
//
|
||
|
// Free the old one since we are using the new one now.
|
||
|
//
|
||
|
RouterFreePrinterNotifyInfo(pPrinterNotifyInfo);
|
||
|
}
|
||
|
|
||
|
pChange->ChangeInfo.pPrinterNotifyInfo->Flags |= dwResult;
|
||
|
break;
|
||
|
|
||
|
case RPC_S_CALL_FAILED_DNE:
|
||
|
|
||
|
//
|
||
|
// On DNE, keep the notification info. We are guarenteed by
|
||
|
// rpc that this means no part of the call executed.
|
||
|
//
|
||
|
if (pPrinterNotifyInfoNew) {
|
||
|
|
||
|
//
|
||
|
// We already have some info. Merge it in
|
||
|
// with the exiting data.
|
||
|
//
|
||
|
pChange->ChangeInfo.pPrinterNotifyInfo = pPrinterNotifyInfo;
|
||
|
|
||
|
if (pChange->ChangeInfo.pPrintHandle) {
|
||
|
|
||
|
AppendPrinterNotifyInfo(pChange->ChangeInfo.pPrintHandle,
|
||
|
dwColor,
|
||
|
pPrinterNotifyInfoNew);
|
||
|
}
|
||
|
RouterFreePrinterNotifyInfo(pPrinterNotifyInfoNew);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
|
||
|
//
|
||
|
// Did it succeed? Maybe, maybe not. Fail it by freeing
|
||
|
// the current one if it exists, then clear the current one
|
||
|
// and set the discard bit.
|
||
|
//
|
||
|
pChange->ChangeInfo.pPrinterNotifyInfo = pPrinterNotifyInfo;
|
||
|
ClearPrinterNotifyInfo(pPrinterNotifyInfo, pChange);
|
||
|
|
||
|
SetDiscardPrinterNotifyInfo(pPrinterNotifyInfo, pChange);
|
||
|
|
||
|
//
|
||
|
// Free the new buffer since we are reusing the old one.
|
||
|
//
|
||
|
if (pPrinterNotifyInfoNew) {
|
||
|
|
||
|
RouterFreePrinterNotifyInfo(pPrinterNotifyInfoNew);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pChange->eStatus &= ~STATUS_CHANGE_ACTIVE;
|
||
|
|
||
|
//
|
||
|
// STATUS_CHANGE_ACTIVE_REQ set, then some notifications came in
|
||
|
// while we were out. Check that we actually do have information
|
||
|
// (it might have been sent with the last RPC) and we aren't closing.
|
||
|
//
|
||
|
if ((pChange->eStatus & STATUS_CHANGE_ACTIVE_REQ) &&
|
||
|
NotifyNeeded(pChange) &&
|
||
|
!(pChange->eStatus & STATUS_CHANGE_CLOSING)) {
|
||
|
|
||
|
DBGMSG(DBG_NOTIFY, ("ThreadNotifyProcessJob: delayed link added 0x%x cRef++ %d\n",
|
||
|
pChange,
|
||
|
pChange->cRef));
|
||
|
|
||
|
pChange->eStatus &= ~STATUS_CHANGE_ACTIVE_REQ;
|
||
|
|
||
|
LinkChange(pChange);
|
||
|
|
||
|
//
|
||
|
// No need to call TMAddJob(&tmStateVar) since this
|
||
|
// thread will pickup the job. If there is a job already waiting,
|
||
|
// then that means it already has a thread spawning to pick it up.
|
||
|
//
|
||
|
}
|
||
|
|
||
|
Done:
|
||
|
//
|
||
|
// Mark ourselves no longer in use. If we were in use when someone
|
||
|
// tried to delete the notify, we need to delete it once we're done.
|
||
|
//
|
||
|
pChange->cRef--;
|
||
|
|
||
|
DBGMSG(DBG_NOTIFY, ("ThreadNotifyProcessJob: Done 0x%x cRef-- %d\n",
|
||
|
pChange,
|
||
|
pChange->cRef));
|
||
|
|
||
|
|
||
|
if (pChange->eStatus & STATUS_CHANGE_CLOSING) {
|
||
|
|
||
|
hNotifyRemote = pChange->hNotifyRemote;
|
||
|
pChange->hNotifyRemote = NULL;
|
||
|
|
||
|
//
|
||
|
// Free the Change struct and close the hNotifyRemote
|
||
|
//
|
||
|
FreeChange(pChange);
|
||
|
|
||
|
EXIT_THREAD_LIST();
|
||
|
|
||
|
CloseReplyRemote(hNotifyRemote);
|
||
|
|
||
|
} else {
|
||
|
|
||
|
EXIT_THREAD_LIST();
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
//
|
||
|
// Usually a macro
|
||
|
//
|
||
|
#ifndef LINKADDFAST
|
||
|
VOID
|
||
|
LinkAdd(
|
||
|
PLINK pLink,
|
||
|
PLINK* ppLinkHead)
|
||
|
|
||
|
/*++
|
||
|
|
||
|
Routine Description:
|
||
|
|
||
|
Adds the item to linked list.
|
||
|
|
||
|
Arguments:
|
||
|
|
||
|
pLink - item to add
|
||
|
|
||
|
ppLinkHead - linked list head pointer
|
||
|
|
||
|
Return Value:
|
||
|
|
||
|
VOID
|
||
|
|
||
|
NOTE: This appends to the tail of the list; the macro must be changed also.
|
||
|
|
||
|
--*/
|
||
|
|
||
|
{
|
||
|
//
|
||
|
// First check if its in the list
|
||
|
//
|
||
|
PLINK pLinkT;
|
||
|
PLINK pLinkLast = NULL;
|
||
|
|
||
|
for(pLinkT=*ppLinkHead; pLinkT; pLinkT=pLinkT->pNext) {
|
||
|
|
||
|
if (pLinkT == pLink) {
|
||
|
|
||
|
DBGMSG(DBG_ERROR, ("LinkAdd: Duplicate link adding!\n"));
|
||
|
}
|
||
|
pLinkLast = pLinkT;
|
||
|
}
|
||
|
|
||
|
if (pLinkLast) {
|
||
|
pLinkLast->pNext = pLink;
|
||
|
} else {
|
||
|
pLink->pNext = *ppLinkHead;
|
||
|
*ppLinkHead = pLink;
|
||
|
}
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
|
||
|
VOID
|
||
|
LinkDelete(
|
||
|
PLINK pLink,
|
||
|
PLINK* ppLinkHead)
|
||
|
|
||
|
/*++
|
||
|
|
||
|
Routine Description:
|
||
|
|
||
|
Removes item from list
|
||
|
|
||
|
Arguments:
|
||
|
|
||
|
pLink - Item to delete
|
||
|
|
||
|
ppLinkHead - pointer to link head
|
||
|
|
||
|
Return Value:
|
||
|
|
||
|
VOID
|
||
|
|
||
|
--*/
|
||
|
|
||
|
{
|
||
|
PLINK pLink2 = *ppLinkHead;
|
||
|
|
||
|
if (!pLink)
|
||
|
return;
|
||
|
|
||
|
//
|
||
|
// Check head case first
|
||
|
//
|
||
|
if (pLink2 == pLink) {
|
||
|
|
||
|
*ppLinkHead = pLink->pNext;
|
||
|
|
||
|
} else {
|
||
|
|
||
|
//
|
||
|
// Scan list to delete
|
||
|
//
|
||
|
for(;
|
||
|
pLink2;
|
||
|
pLink2=pLink2->pNext) {
|
||
|
|
||
|
if (pLink == pLink2->pNext) {
|
||
|
|
||
|
pLink2->pNext = pLink->pNext;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pLink->pNext = NULL;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
|