af3e5506234d821633e841738040b51ef53c83bc
[public/netxms.git] / src / libnxcl / objects.cpp
1 /*
2 ** NetXMS - Network Management System
3 ** Client Library
4 ** Copyright (C) 2004 Victor Kirhenshtein
5 **
6 ** This program is free software; you can redistribute it and/or modify
7 ** it under the terms of the GNU General Public License as published by
8 ** the Free Software Foundation; either version 2 of the License, or
9 ** (at your option) any later version.
10 **
11 ** This program is distributed in the hope that it will be useful,
12 ** but WITHOUT ANY WARRANTY; without even the implied warranty of
13 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 ** GNU General Public License for more details.
15 **
16 ** You should have received a copy of the GNU General Public License
17 ** along with this program; if not, write to the Free Software
18 ** Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19 **
20 ** $module: objects.cpp
21 **
22 **/
23
24 #include "libnxcl.h"
25
26
27 //
28 // Static data
29 //
30
31 static DWORD m_dwNumObjects = 0;
32 static INDEX *m_pIndexById = NULL;
33 static MUTEX m_mutexIndexAccess;
34
35
36 //
37 // Initialize object handling module
38 //
39
40 void ObjectsInit(void)
41 {
42 m_mutexIndexAccess = MutexCreate();
43 }
44
45
46 //
47 // Perform binary search on index
48 // Returns INVALID_INDEX if key not found or position of appropriate network object
49 // We assume that pIndex == NULL will not be passed
50 //
51
52 static DWORD SearchIndex(INDEX *pIndex, DWORD dwIndexSize, DWORD dwKey)
53 {
54 DWORD dwFirst, dwLast, dwMid;
55
56 dwFirst = 0;
57 dwLast = dwIndexSize - 1;
58
59 if ((dwKey < pIndex[0].dwKey) || (dwKey > pIndex[dwLast].dwKey))
60 return INVALID_INDEX;
61
62 while(dwFirst < dwLast)
63 {
64 dwMid = (dwFirst + dwLast) / 2;
65 if (dwKey == pIndex[dwMid].dwKey)
66 return dwMid;
67 if (dwKey < pIndex[dwMid].dwKey)
68 dwLast = dwMid - 1;
69 else
70 dwFirst = dwMid + 1;
71 }
72
73 if (dwKey == pIndex[dwLast].dwKey)
74 return dwLast;
75
76 return INVALID_INDEX;
77 }
78
79
80 //
81 // Index comparision callback for qsort()
82 //
83
84 static int IndexCompare(const void *pArg1, const void *pArg2)
85 {
86 return ((INDEX *)pArg1)->dwKey < ((INDEX *)pArg2)->dwKey ? -1 :
87 (((INDEX *)pArg1)->dwKey > ((INDEX *)pArg2)->dwKey ? 1 : 0);
88 }
89
90
91 //
92 // Add object to list
93 //
94
95 static void AddObject(NXC_OBJECT *pObject, BOOL bSortIndex)
96 {
97 DebugPrintf("AddObject(id:%ld, name:\"%s\")", pObject->dwId, pObject->szName);
98 NXCLockObjectIndex();
99 m_pIndexById = (INDEX *)realloc(m_pIndexById, sizeof(INDEX) * (m_dwNumObjects + 1));
100 m_pIndexById[m_dwNumObjects].dwKey = pObject->dwId;
101 m_pIndexById[m_dwNumObjects].pObject = pObject;
102 m_dwNumObjects++;
103 if (bSortIndex)
104 qsort(m_pIndexById, m_dwNumObjects, sizeof(INDEX), IndexCompare);
105 NXCUnlockObjectIndex();
106 }
107
108
109 //
110 // Replace object's data in list
111 //
112
113 static void ReplaceObject(NXC_OBJECT *pObject, NXC_OBJECT *pNewObject)
114 {
115 DebugPrintf("ReplaceObject(id:%ld, name:\"%s\")", pObject->dwId, pObject->szName);
116 if (pObject->iClass == OBJECT_CONTAINER)
117 safe_free(pObject->container.pszDescription);
118 safe_free(pObject->pdwChildList);
119 safe_free(pObject->pdwParentList);
120 safe_free(pObject->pAccessList);
121 memcpy(pObject, pNewObject, sizeof(NXC_OBJECT));
122 free(pNewObject);
123 }
124
125
126 //
127 // Destroy object
128 //
129
130 static void DestroyObject(NXC_OBJECT *pObject)
131 {
132 DebugPrintf("DestroyObject(id:%ld, name:\"%s\")", pObject->dwId, pObject->szName);
133 if (pObject->iClass == OBJECT_CONTAINER)
134 safe_free(pObject->container.pszDescription);
135 safe_free(pObject->pdwChildList);
136 safe_free(pObject->pdwParentList);
137 safe_free(pObject->pAccessList);
138 free(pObject);
139 }
140
141
142 //
143 // Create new object from message
144 //
145
146 static NXC_OBJECT *NewObjectFromMsg(CSCPMessage *pMsg)
147 {
148 NXC_OBJECT *pObject;
149 DWORD i, dwId1, dwId2;
150
151 // Allocate memory for new object structure
152 pObject = (NXC_OBJECT *)malloc(sizeof(NXC_OBJECT));
153 memset(pObject, 0, sizeof(NXC_OBJECT));
154
155 // Common attributes
156 pObject->dwId = pMsg->GetVariableLong(VID_OBJECT_ID);
157 pObject->iClass = pMsg->GetVariableShort(VID_OBJECT_CLASS);
158 pMsg->GetVariableStr(VID_OBJECT_NAME, pObject->szName, MAX_OBJECT_NAME);
159 pObject->iStatus = pMsg->GetVariableShort(VID_OBJECT_STATUS);
160 pObject->dwIpAddr = pMsg->GetVariableLong(VID_IP_ADDRESS);
161 pObject->bIsDeleted = pMsg->GetVariableShort(VID_IS_DELETED);
162 pObject->dwImage = pMsg->GetVariableLong(VID_IMAGE_ID);
163
164 // Parents
165 pObject->dwNumParents = pMsg->GetVariableLong(VID_PARENT_CNT);
166 pObject->pdwParentList = (DWORD *)malloc(sizeof(DWORD) * pObject->dwNumParents);
167 for(i = 0, dwId1 = VID_PARENT_ID_BASE; i < pObject->dwNumParents; i++, dwId1++)
168 pObject->pdwParentList[i] = pMsg->GetVariableLong(dwId1);
169
170 // Childs
171 pObject->dwNumChilds = pMsg->GetVariableLong(VID_CHILD_CNT);
172 pObject->pdwChildList = (DWORD *)malloc(sizeof(DWORD) * pObject->dwNumChilds);
173 for(i = 0, dwId1 = VID_CHILD_ID_BASE; i < pObject->dwNumChilds; i++, dwId1++)
174 pObject->pdwChildList[i] = pMsg->GetVariableLong(dwId1);
175
176 // Access control
177 pObject->bInheritRights = pMsg->GetVariableShort(VID_INHERIT_RIGHTS);
178 pObject->dwAclSize = pMsg->GetVariableLong(VID_ACL_SIZE);
179 pObject->pAccessList = (NXC_ACL_ENTRY *)malloc(sizeof(NXC_ACL_ENTRY) * pObject->dwAclSize);
180 for(i = 0, dwId1 = VID_ACL_USER_BASE, dwId2 = VID_ACL_RIGHTS_BASE;
181 i < pObject->dwAclSize; i++, dwId1++, dwId2++)
182 {
183 pObject->pAccessList[i].dwUserId = pMsg->GetVariableLong(dwId1);
184 pObject->pAccessList[i].dwAccessRights = pMsg->GetVariableLong(dwId2);
185 }
186
187 // Class-specific attributes
188 switch(pObject->iClass)
189 {
190 case OBJECT_INTERFACE:
191 pObject->iface.dwIpNetMask = pMsg->GetVariableLong(VID_IP_NETMASK);
192 pObject->iface.dwIfIndex = pMsg->GetVariableLong(VID_IF_INDEX);
193 pObject->iface.dwIfType = pMsg->GetVariableLong(VID_IF_TYPE);
194 break;
195 case OBJECT_NODE:
196 pObject->node.dwFlags = pMsg->GetVariableLong(VID_FLAGS);
197 pObject->node.dwDiscoveryFlags = pMsg->GetVariableLong(VID_DISCOVERY_FLAGS);
198 pObject->node.wAgentPort = pMsg->GetVariableShort(VID_AGENT_PORT);
199 pObject->node.wAuthMethod = pMsg->GetVariableShort(VID_AUTH_METHOD);
200 pMsg->GetVariableStr(VID_SHARED_SECRET, pObject->node.szSharedSecret, MAX_SECRET_LENGTH);
201 pMsg->GetVariableStr(VID_COMMUNITY_STRING, pObject->node.szCommunityString, MAX_COMMUNITY_LENGTH);
202 pMsg->GetVariableStr(VID_SNMP_OID, pObject->node.szObjectId, MAX_OID_LENGTH);
203 break;
204 case OBJECT_SUBNET:
205 pObject->subnet.dwIpNetMask = pMsg->GetVariableLong(VID_IP_NETMASK);
206 break;
207 case OBJECT_CONTAINER:
208 pObject->container.dwCategory = pMsg->GetVariableLong(VID_CATEGORY);
209 pObject->container.pszDescription = pMsg->GetVariableStr(VID_DESCRIPTION);
210 break;
211 default:
212 break;
213 }
214
215 return pObject;
216 }
217
218
219 //
220 // Process object information received from server
221 //
222
223 void ProcessObjectUpdate(CSCPMessage *pMsg)
224 {
225 NXC_OBJECT *pObject, *pNewObject;
226
227 switch(pMsg->GetCode())
228 {
229 case CMD_OBJECT_LIST_END:
230 NXCLockObjectIndex();
231 qsort(m_pIndexById, m_dwNumObjects, sizeof(INDEX), IndexCompare);
232 NXCUnlockObjectIndex();
233 CompleteSync(RCC_SUCCESS);
234 break;
235 case CMD_OBJECT:
236 DebugPrintf("RECV_OBJECT: ID=%d Name=\"%s\" Class=%d", pMsg->GetVariableLong(VID_OBJECT_ID),
237 pMsg->GetVariableStr(VID_OBJECT_NAME), pMsg->GetVariableShort(VID_OBJECT_CLASS));
238
239 // Create new object from message and add it to list
240 pObject = NewObjectFromMsg(pMsg);
241 AddObject(pObject, FALSE);
242 break;
243 case CMD_OBJECT_UPDATE:
244 pNewObject = NewObjectFromMsg(pMsg);
245 pObject = NXCFindObjectById(pNewObject->dwId);
246 if (pObject == NULL)
247 {
248 AddObject(pNewObject, TRUE);
249 pObject = pNewObject;
250 }
251 else
252 {
253 ReplaceObject(pObject, pNewObject);
254 }
255 CallEventHandler(NXC_EVENT_OBJECT_CHANGED, pObject->dwId, pObject);
256 break;
257 default:
258 break;
259 }
260 }
261
262
263 //
264 // Synchronize objects with the server
265 // This function is NOT REENTRANT
266 //
267
268 DWORD LIBNXCL_EXPORTABLE NXCSyncObjects(void)
269 {
270 CSCPMessage msg;
271 DWORD dwRetCode, dwRqId;
272
273 dwRqId = g_dwMsgId++;
274 PrepareForSync();
275
276 msg.SetCode(CMD_GET_OBJECTS);
277 msg.SetId(dwRqId);
278 SendMsg(&msg);
279
280 dwRetCode = WaitForRCC(dwRqId);
281
282 // If request was successful, wait for object list end or for disconnection
283 if (dwRetCode == RCC_SUCCESS)
284 dwRetCode = WaitForSync(INFINITE);
285
286 return dwRetCode;
287 }
288
289
290 //
291 // Find object by ID
292 //
293
294 NXC_OBJECT LIBNXCL_EXPORTABLE *NXCFindObjectById(DWORD dwId)
295 {
296 DWORD dwPos;
297 NXC_OBJECT *pObject;
298
299 NXCLockObjectIndex();
300 dwPos = SearchIndex(m_pIndexById, m_dwNumObjects, dwId);
301 pObject = (dwPos == INVALID_INDEX) ? NULL : m_pIndexById[dwPos].pObject;
302 NXCUnlockObjectIndex();
303 return pObject;
304 }
305
306
307 //
308 // Enumerate all objects
309 //
310
311 void LIBNXCL_EXPORTABLE NXCEnumerateObjects(BOOL (* pHandler)(NXC_OBJECT *))
312 {
313 DWORD i;
314
315 NXCLockObjectIndex();
316 for(i = 0; i < m_dwNumObjects; i++)
317 if (!pHandler(m_pIndexById[i].pObject))
318 break;
319 NXCUnlockObjectIndex();
320 }
321
322
323 //
324 // Get topology root ("Entire Network") object
325 //
326
327 NXC_OBJECT LIBNXCL_EXPORTABLE *NXCGetTopologyRootObject(void)
328 {
329 if (m_dwNumObjects > 0)
330 if (m_pIndexById[0].dwKey == 1)
331 return m_pIndexById[0].pObject;
332 return NULL;
333 }
334
335
336 //
337 // Get service tree root ("All Services") object
338 //
339
340 NXC_OBJECT LIBNXCL_EXPORTABLE *NXCGetServiceRootObject(void)
341 {
342 if (m_dwNumObjects > 1)
343 if (m_pIndexById[1].dwKey == 2)
344 return m_pIndexById[1].pObject;
345 return NULL;
346 }
347
348
349 //
350 // Get pointer to first object on objects' list and entire number of objects
351 //
352
353 void LIBNXCL_EXPORTABLE *NXCGetObjectIndex(DWORD *pdwNumObjects)
354 {
355 if (pdwNumObjects != NULL)
356 *pdwNumObjects = m_dwNumObjects;
357 return m_pIndexById;
358 }
359
360
361 //
362 // Lock object index
363 //
364
365 void LIBNXCL_EXPORTABLE NXCLockObjectIndex(void)
366 {
367 MutexLock(m_mutexIndexAccess, INFINITE);
368 }
369
370
371 //
372 // Unlock object index
373 //
374
375 void LIBNXCL_EXPORTABLE NXCUnlockObjectIndex(void)
376 {
377 MutexUnlock(m_mutexIndexAccess);
378 }
379
380
381 //
382 // Find object by name
383 //
384
385 NXC_OBJECT LIBNXCL_EXPORTABLE *NXCFindObjectByName(char *pszName)
386 {
387 NXC_OBJECT *pObject = NULL;
388 DWORD i;
389
390 if (pszName != NULL)
391 if (*pszName != 0)
392 {
393 NXCLockObjectIndex();
394
395 for(i = 0; i < m_dwNumObjects; i++)
396 if (MatchString(pszName, m_pIndexById[i].pObject->szName, FALSE))
397 {
398 pObject = m_pIndexById[i].pObject;
399 break;
400 }
401
402 NXCUnlockObjectIndex();
403 }
404 return pObject;
405 }
406
407
408 //
409 // Modify object
410 //
411
412 DWORD LIBNXCL_EXPORTABLE NXCModifyObject(NXC_OBJECT_UPDATE *pUpdate)
413 {
414 CSCPMessage msg;
415 DWORD dwRqId;
416
417 dwRqId = g_dwMsgId++;
418
419 // Build request message
420 msg.SetCode(CMD_MODIFY_OBJECT);
421 msg.SetId(dwRqId);
422 msg.SetVariable(VID_OBJECT_ID, pUpdate->dwObjectId);
423 if (pUpdate->dwFlags & OBJ_UPDATE_NAME)
424 msg.SetVariable(VID_OBJECT_NAME, pUpdate->pszName);
425 if (pUpdate->dwFlags & OBJ_UPDATE_AGENT_PORT)
426 msg.SetVariable(VID_AGENT_PORT, (WORD)pUpdate->iAgentPort);
427 if (pUpdate->dwFlags & OBJ_UPDATE_AGENT_AUTH)
428 msg.SetVariable(VID_AUTH_METHOD, (WORD)pUpdate->iAuthType);
429 if (pUpdate->dwFlags & OBJ_UPDATE_AGENT_SECRET)
430 msg.SetVariable(VID_SHARED_SECRET, pUpdate->pszSecret);
431 if (pUpdate->dwFlags & OBJ_UPDATE_SNMP_COMMUNITY)
432 msg.SetVariable(VID_COMMUNITY_STRING, pUpdate->pszCommunity);
433 if (pUpdate->dwFlags & OBJ_UPDATE_IMAGE)
434 msg.SetVariable(VID_IMAGE_ID, pUpdate->dwImage);
435 if (pUpdate->dwFlags & OBJ_UPDATE_ACL)
436 {
437 DWORD i, dwId1, dwId2;
438
439 msg.SetVariable(VID_ACL_SIZE, pUpdate->dwAclSize);
440 msg.SetVariable(VID_INHERIT_RIGHTS, (WORD)pUpdate->bInheritRights);
441 for(i = 0, dwId1 = VID_ACL_USER_BASE, dwId2 = VID_ACL_RIGHTS_BASE;
442 i < pUpdate->dwAclSize; i++, dwId1++, dwId2++)
443 {
444 msg.SetVariable(dwId1, pUpdate->pAccessList[i].dwUserId);
445 msg.SetVariable(dwId2, pUpdate->pAccessList[i].dwAccessRights);
446 }
447 }
448
449 // Send request
450 SendMsg(&msg);
451
452 // Wait for reply
453 return WaitForRCC(dwRqId);
454 }
455
456
457 //
458 // Set object's mamagement status
459 //
460
461 DWORD LIBNXCL_EXPORTABLE NXCSetObjectMgmtStatus(DWORD dwObjectId, BOOL bIsManaged)
462 {
463 CSCPMessage msg;
464 DWORD dwRqId;
465
466 dwRqId = g_dwMsgId++;
467
468 // Build request message
469 msg.SetCode(CMD_SET_OBJECT_MGMT_STATUS);
470 msg.SetId(dwRqId);
471 msg.SetVariable(VID_OBJECT_ID, dwObjectId);
472 msg.SetVariable(VID_MGMT_STATUS, (WORD)bIsManaged);
473
474 // Send request
475 SendMsg(&msg);
476
477 // Wait for reply
478 return WaitForRCC(dwRqId);
479 }
480
481
482 //
483 // Create new object
484 //
485
486 DWORD LIBNXCL_EXPORTABLE NXCCreateObject(NXC_OBJECT_CREATE_INFO *pCreateInfo, DWORD *pdwObjectId)
487 {
488 CSCPMessage msg, *pResponce;
489 DWORD dwRqId, dwRetCode;
490
491 dwRqId = g_dwMsgId++;
492
493 // Build request message
494 msg.SetCode(CMD_CREATE_OBJECT);
495 msg.SetId(dwRqId);
496 msg.SetVariable(VID_PARENT_ID, pCreateInfo->dwParentId);
497 msg.SetVariable(VID_OBJECT_CLASS, (WORD)pCreateInfo->iClass);
498 msg.SetVariable(VID_OBJECT_NAME, pCreateInfo->pszName);
499 switch(pCreateInfo->iClass)
500 {
501 case OBJECT_NODE:
502 msg.SetVariable(VID_IP_ADDRESS, pCreateInfo->cs.node.dwIpAddr);
503 msg.SetVariable(VID_IP_NETMASK, pCreateInfo->cs.node.dwNetMask);
504 break;
505 case OBJECT_CONTAINER:
506 msg.SetVariable(VID_CATEGORY, pCreateInfo->cs.container.dwCategory);
507 msg.SetVariable(VID_DESCRIPTION, pCreateInfo->cs.container.pszDescription);
508 break;
509 default:
510 break;
511 }
512
513 // Send request
514 SendMsg(&msg);
515
516 // Wait for responce
517 pResponce = WaitForMessage(CMD_REQUEST_COMPLETED, dwRqId, 60000);
518 if (pResponce != NULL)
519 {
520 dwRetCode = pResponce->GetVariableLong(VID_RCC);
521 if (dwRetCode == RCC_SUCCESS)
522 {
523 *pdwObjectId = pResponce->GetVariableLong(VID_OBJECT_ID);
524 }
525 delete pResponce;
526 }
527 else
528 {
529 dwRetCode = RCC_TIMEOUT;
530 }
531
532 return dwRetCode;
533 }
534
535
536 //
537 // Bind/unbind objects
538 //
539
540 static DWORD ChangeObjectBinding(DWORD dwParentObject, DWORD dwChildObject, BOOL bBind)
541 {
542 CSCPMessage msg;
543 DWORD dwRqId;
544
545 dwRqId = g_dwMsgId++;
546
547 // Build request message
548 msg.SetCode(bBind ? CMD_BIND_OBJECT : CMD_UNBIND_OBJECT);
549 msg.SetId(dwRqId);
550 msg.SetVariable(VID_PARENT_ID, dwParentObject);
551 msg.SetVariable(VID_CHILD_ID, dwChildObject);
552
553 // Send request
554 SendMsg(&msg);
555
556 // Wait for reply
557 return WaitForRCC(dwRqId);
558 }
559
560
561 //
562 // Bind object
563 //
564
565 DWORD LIBNXCL_EXPORTABLE NXCBindObject(DWORD dwParentObject, DWORD dwChildObject)
566 {
567 return ChangeObjectBinding(dwParentObject, dwChildObject, TRUE);
568 }
569
570
571 //
572 // Unbind object
573 //
574
575 DWORD LIBNXCL_EXPORTABLE NXCUnbindObject(DWORD dwParentObject, DWORD dwChildObject)
576 {
577 return ChangeObjectBinding(dwParentObject, dwChildObject, FALSE);
578 }
579
580
581 //
582 // Delete object
583 //
584
585 DWORD LIBNXCL_EXPORTABLE NXCDeleteObject(DWORD dwObject)
586 {
587 CSCPMessage msg;
588 DWORD dwRqId;
589
590 dwRqId = g_dwMsgId++;
591
592 // Build request message
593 msg.SetCode(CMD_DELETE_OBJECT);
594 msg.SetId(dwRqId);
595 msg.SetVariable(VID_OBJECT_ID, dwObject);
596
597 // Send request
598 SendMsg(&msg);
599
600 // Wait for reply
601 return WaitForRCC(dwRqId);
602 }
603
604
605 //
606 // Load container categories
607 //
608
609 DWORD LIBNXCL_EXPORTABLE NXCLoadCCList(NXC_CC_LIST **ppList)
610 {
611 CSCPMessage msg, *pResponce;
612 DWORD dwRqId, dwRetCode = RCC_SUCCESS, dwNumCats = 0, dwCatId = 0;
613
614 dwRqId = g_dwMsgId++;
615
616 msg.SetCode(CMD_GET_CONTAINER_CAT_LIST);
617 msg.SetId(dwRqId);
618 SendMsg(&msg);
619
620 *ppList = (NXC_CC_LIST *)malloc(sizeof(NXC_CC_LIST));
621 (*ppList)->dwNumElements = 0;
622 (*ppList)->pElements = NULL;
623
624 do
625 {
626 pResponce = WaitForMessage(CMD_CONTAINER_CAT_DATA, dwRqId, 2000);
627 if (pResponce != NULL)
628 {
629 dwCatId = pResponce->GetVariableLong(VID_CATEGORY_ID);
630 if (dwCatId != 0) // 0 is end of list indicator
631 {
632 (*ppList)->pElements = (NXC_CONTAINER_CATEGORY *)realloc((*ppList)->pElements,
633 sizeof(NXC_CONTAINER_CATEGORY) * ((*ppList)->dwNumElements + 1));
634 (*ppList)->pElements[(*ppList)->dwNumElements].dwId = dwCatId;
635 (*ppList)->pElements[(*ppList)->dwNumElements].dwImageId =
636 pResponce->GetVariableLong(VID_IMAGE_ID);
637 pResponce->GetVariableStr(VID_CATEGORY_NAME,
638 (*ppList)->pElements[(*ppList)->dwNumElements].szName, MAX_OBJECT_NAME);
639 (*ppList)->pElements[(*ppList)->dwNumElements].pszDescription =
640 pResponce->GetVariableStr(VID_DESCRIPTION);
641 (*ppList)->dwNumElements++;
642 }
643 delete pResponce;
644 }
645 else
646 {
647 dwRetCode = RCC_TIMEOUT;
648 dwCatId = 0;
649 }
650 }
651 while(dwCatId != 0);
652
653 // Destroy results on failure
654 if (dwRetCode != RCC_SUCCESS)
655 {
656 safe_free((*ppList)->pElements);
657 free(*ppList);
658 *ppList = NULL;
659 }
660
661 return dwRetCode;
662 }
663
664
665 //
666 // Destroy list of container categories
667 //
668
669 void LIBNXCL_EXPORTABLE NXCDestroyCCList(NXC_CC_LIST *pList)
670 {
671 DWORD i;
672
673 if (pList == NULL)
674 return;
675
676 for(i = 0; i < pList->dwNumElements; i++)
677 safe_free(pList->pElements[i].pszDescription);
678 safe_free(pList->pElements);
679 free(pList);
680 }