615 lines
18 KiB
C++
615 lines
18 KiB
C++
//+---------------------------------------------------------------------------
|
|
//
|
|
// Microsoft Windows
|
|
// Copyright (C) Microsoft Corporation 1996-2001.
|
|
//
|
|
// File: uithread.cpp
|
|
//
|
|
// Contents: implementation of CUIThread
|
|
//
|
|
//----------------------------------------------------------------------------
|
|
|
|
#include "stdafx.h"
|
|
#include "wsecmgr.h"
|
|
#include "resource.h"
|
|
#include <accctrl.h>
|
|
#include "servperm.h"
|
|
#include "stdafx.h"
|
|
#include "wsecmgr.h"
|
|
#include "snapmgr.h"
|
|
#include "util.h"
|
|
#include "UIThread.h"
|
|
#include "attr.h"
|
|
#include "aaudit.h"
|
|
#include "aenable.h"
|
|
#include "AMember.h"
|
|
#include "anumber.h"
|
|
#include "AObject.h"
|
|
#include "ARet.h"
|
|
#include "ARight.h"
|
|
#include "aservice.h"
|
|
#include "astring.h"
|
|
#include "CAudit.h"
|
|
#include "CEnable.h"
|
|
#include "CGroup.h"
|
|
#include "CName.h"
|
|
#include "CNumber.h"
|
|
#include "cobject.h"
|
|
#include "CPrivs.h"
|
|
#include "CRet.h"
|
|
#include "cservice.h"
|
|
#include "regvldlg.h"
|
|
#include "perfanal.h"
|
|
#include "applcnfg.h"
|
|
#include "wrapper.h"
|
|
#include "locdesc.h"
|
|
#include "profdesc.h"
|
|
#include "newprof.h"
|
|
#include "laudit.h"
|
|
#include "lenable.h"
|
|
#include "lret.h"
|
|
#include "lnumber.h"
|
|
#include "lstring.h"
|
|
#include "lright.h"
|
|
#include "achoice.h"
|
|
#include "cchoice.h"
|
|
#include "lchoice.h"
|
|
#include "dattrs.h"
|
|
#include "lflags.h"
|
|
#include "aflags.h"
|
|
#include "multisz.h"
|
|
|
|
#ifdef _DEBUG
|
|
#define new DEBUG_NEW
|
|
#undef THIS_FILE
|
|
static char THIS_FILE[] = __FILE__;
|
|
#endif
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
// CUIThread
|
|
|
|
IMPLEMENT_DYNCREATE(CUIThread, CWinThread)
|
|
|
|
CUIThread::CUIThread()
|
|
{
|
|
}
|
|
|
|
CUIThread::~CUIThread()
|
|
{
|
|
}
|
|
|
|
BOOL CUIThread::InitInstance()
|
|
{
|
|
// TODO: perform and per-thread initialization here
|
|
return TRUE;
|
|
}
|
|
|
|
int CUIThread::ExitInstance()
|
|
{
|
|
// TODO: perform any per-thread cleanup here
|
|
return CWinThread::ExitInstance();
|
|
}
|
|
|
|
BEGIN_MESSAGE_MAP(CUIThread, CWinThread)
|
|
//{{AFX_MSG_MAP(CUIThread)
|
|
// NOTE - the ClassWizard will add and remove mapping macros here.
|
|
//}}AFX_MSG_MAP
|
|
ON_THREAD_MESSAGE( SCEM_APPLY_PROFILE, OnApplyProfile)
|
|
ON_THREAD_MESSAGE( SCEM_ANALYZE_PROFILE, OnAnalyzeProfile)
|
|
ON_THREAD_MESSAGE( SCEM_DESCRIBE_PROFILE, OnDescribeProfile)
|
|
ON_THREAD_MESSAGE( SCEM_DESCRIBE_LOCATION, OnDescribeLocation)
|
|
ON_THREAD_MESSAGE( SCEM_DESTROY_DIALOG, OnDestroyDialog)
|
|
ON_THREAD_MESSAGE( SCEM_NEW_CONFIGURATION, OnNewConfiguration)
|
|
ON_THREAD_MESSAGE( SCEM_ADD_PROPSHEET, OnAddPropSheet)
|
|
END_MESSAGE_MAP()
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
// CUIThread message handlers
|
|
|
|
//+--------------------------------------------------------------------------
|
|
//
|
|
// Method: DefaultLogFile
|
|
//
|
|
// Synopsis: Find the default log file for the inf file last applied
|
|
// pass back the log file via the out parameter strLogFile
|
|
//
|
|
//---------------------------------------------------------------------------
|
|
void
|
|
CUIThread::DefaultLogFile(CComponentDataImpl *pCDI,GWD_TYPES LogID,LPCTSTR szBase, CString& strLogFile)
|
|
{
|
|
//
|
|
// Base log file on the db passed in
|
|
//
|
|
CString strDefExt;
|
|
strDefExt.LoadString(IDS_LOGFILE_DEF_EXT);
|
|
// make sure that the extension includes '.'
|
|
if (strDefExt.GetLength() > 0 && strDefExt[0] != L'.')
|
|
{
|
|
CString tempExt = strDefExt;
|
|
strDefExt = L'.';
|
|
strDefExt += tempExt;
|
|
}
|
|
|
|
CString strInfFile = szBase;
|
|
|
|
//
|
|
// Get the default working directory
|
|
//
|
|
LPTSTR szDir = NULL;
|
|
if (pCDI->GetWorkingDir(LogID,
|
|
&szDir,
|
|
FALSE,
|
|
FALSE)) {
|
|
strLogFile = szDir;
|
|
LocalFree(szDir);
|
|
szDir = NULL;
|
|
}
|
|
|
|
if (strLogFile.Right(1) != TEXT("\\")) {
|
|
strLogFile += L"\\";
|
|
}
|
|
|
|
if ( strInfFile.GetLength() < 5) {
|
|
//
|
|
// The default log file.
|
|
//
|
|
strLogFile += TEXT("SceStus") + strDefExt;
|
|
} else {
|
|
int nFilePartIndex = 0;
|
|
int nFilePartCount = 0;
|
|
//
|
|
// +1 to change index base from 0 to 1 for the Mid
|
|
//
|
|
nFilePartIndex = strInfFile.ReverseFind(L'\\') +1;
|
|
nFilePartCount = strInfFile.ReverseFind(L'.') - nFilePartIndex;
|
|
strLogFile += strInfFile.Mid(nFilePartIndex,nFilePartCount) + strDefExt;
|
|
}
|
|
}
|
|
//+--------------------------------------------------------------------------
|
|
//
|
|
// Method: OnApplyProfile
|
|
//
|
|
// Synopsis: Create and display a dialog for applying a profile to the
|
|
// system
|
|
//
|
|
// Arguments: [uParam] - A string with the name of the database to assign to
|
|
// [lParam] - A pointer to the CComponentDataImpl
|
|
//
|
|
//---------------------------------------------------------------------------
|
|
void
|
|
CUIThread::OnApplyProfile(WPARAM uParam, LPARAM lParam) {
|
|
CComponentDataImpl *pCDI = NULL;
|
|
CApplyConfiguration *pAP = NULL;
|
|
CWnd cwndParent;
|
|
|
|
pCDI = reinterpret_cast<CComponentDataImpl*>(lParam);
|
|
|
|
//
|
|
// Look for a preexisting version of this dialog in pCDI's cache.
|
|
// If it's not there then create a new one and add it.
|
|
//
|
|
pAP = (CApplyConfiguration *)pCDI->GetPopupDialog(IDM_APPLY);
|
|
if (NULL == pAP) {
|
|
pAP = new CApplyConfiguration;
|
|
if (NULL == pAP) {
|
|
return;
|
|
}
|
|
pCDI->AddPopupDialog(IDM_APPLY,pAP);
|
|
}
|
|
|
|
pAP->m_strDataBase = reinterpret_cast<LPCTSTR>(uParam);
|
|
DefaultLogFile(pCDI,GWD_CONFIGURE_LOG, pAP->m_strDataBase, pAP->m_strLogFile);
|
|
pAP->SetComponentData(pCDI);
|
|
|
|
if (!pAP->GetSafeHwnd())
|
|
{
|
|
cwndParent.Attach(pCDI->GetParentWindow());
|
|
|
|
CThemeContextActivator activator;
|
|
pAP->Create(IDD_APPLY_CONFIGURATION,&cwndParent);
|
|
cwndParent.Detach();
|
|
}
|
|
|
|
pAP->UpdateData(FALSE);
|
|
pAP->BringWindowToTop();
|
|
pAP->ShowWindow(SW_SHOWNORMAL);
|
|
}
|
|
|
|
//+--------------------------------------------------------------------------
|
|
//
|
|
// Method: OnAnalyzeProfile
|
|
//
|
|
// Synopsis: Create and display a dialog for applying a profile to the
|
|
// system
|
|
//
|
|
// Arguments: [uParam] - A string with the name of the database to assign to
|
|
// [lParam] - A pointer to the CComponentDataImpl
|
|
//
|
|
//---------------------------------------------------------------------------
|
|
void
|
|
CUIThread::OnAnalyzeProfile(WPARAM uParam, LPARAM lParam) {
|
|
CComponentDataImpl *pCDI = NULL;
|
|
CPerformAnalysis *pPA = NULL;
|
|
CWnd cwndParent;
|
|
|
|
pCDI = reinterpret_cast<CComponentDataImpl*>(lParam);
|
|
|
|
//
|
|
// Look for a preexisting version of this dialog in pCDI's cache.
|
|
// If it's not there then create a new one and add it.
|
|
//
|
|
pPA = (CPerformAnalysis *)pCDI->GetPopupDialog(IDM_ANALYZE);
|
|
if (NULL == pPA) {
|
|
pPA = new CPerformAnalysis (0, 0);
|
|
if (NULL == pPA) {
|
|
return;
|
|
}
|
|
|
|
pPA->m_strDataBase = reinterpret_cast<LPCTSTR>(uParam);
|
|
DefaultLogFile(pCDI,GWD_ANALYSIS_LOG, pPA->m_strDataBase, pPA->m_strLogFile);
|
|
pPA->SetComponentData(pCDI);
|
|
|
|
pCDI->AddPopupDialog(IDM_ANALYZE,pPA);
|
|
}
|
|
|
|
if (!pPA->GetSafeHwnd())
|
|
{
|
|
cwndParent.Attach(pCDI->GetParentWindow());
|
|
CThemeContextActivator activator;
|
|
pPA->Create(IDD_PERFORM_ANALYSIS,&cwndParent);
|
|
cwndParent.Detach();
|
|
}
|
|
|
|
pPA->BringWindowToTop();
|
|
pPA->ShowWindow(SW_SHOWNORMAL);
|
|
}
|
|
|
|
|
|
//+--------------------------------------------------------------------------
|
|
//
|
|
// Method: OnDescribeProfile
|
|
//
|
|
// Synopsis: Create and display a dialog for editing a profile's description
|
|
//
|
|
// Arguments: [uParam] - A pointer to the CFolder for the object
|
|
// [lParam] - The CComponentDataImpl owning the scope pane
|
|
//
|
|
//---------------------------------------------------------------------------
|
|
void
|
|
CUIThread::OnDescribeProfile(WPARAM uParam, LPARAM lParam)
|
|
{
|
|
CSetProfileDescription *pSPD;
|
|
CFolder *pFolder;
|
|
CComponentDataImpl *pCDI;
|
|
CWnd cwndParent;
|
|
|
|
LPTSTR szDesc;
|
|
|
|
pFolder = reinterpret_cast<CFolder*>(uParam);
|
|
pCDI = reinterpret_cast<CComponentDataImpl*>(lParam);
|
|
LONG_PTR dwKey = DLG_KEY(pFolder, CSetProfileDescription::IDD);
|
|
|
|
//
|
|
// Look for a preexisting version of this dialog in pCDI's cache.
|
|
// If it's not there then create a new one and add it.
|
|
//
|
|
pSPD = (CSetProfileDescription *)pCDI->GetPopupDialog( dwKey );
|
|
if (NULL == pSPD) {
|
|
pSPD = new CSetProfileDescription;
|
|
if (NULL == pSPD) {
|
|
return;
|
|
}
|
|
|
|
pCDI->AddPopupDialog( dwKey, pSPD);
|
|
}
|
|
|
|
|
|
|
|
if (!pSPD->GetSafeHwnd())
|
|
{
|
|
if (GetProfileDescription(pFolder->GetInfFile(),&szDesc))
|
|
pSPD->m_strDesc = szDesc;
|
|
|
|
pSPD->Initialize(pFolder,pCDI);
|
|
|
|
cwndParent.Attach(pCDI->GetParentWindow());
|
|
CThemeContextActivator activator;
|
|
pSPD->Create(IDD_SET_DESCRIPTION,&cwndParent);
|
|
cwndParent.Detach();
|
|
}
|
|
|
|
pSPD->UpdateData(FALSE);
|
|
pSPD->BringWindowToTop();
|
|
pSPD->ShowWindow(SW_SHOWNORMAL);
|
|
|
|
}
|
|
|
|
|
|
|
|
//+--------------------------------------------------------------------------
|
|
//
|
|
// Method: OnDescribeLocation
|
|
//
|
|
// Synopsis: Create and display a dialog for editing a Location's description
|
|
//
|
|
// Arguments: [uParam] - A pointer to the CFolder for the object
|
|
// [lParam] - The CComponentDataImpl owning the scope pane
|
|
//
|
|
//---------------------------------------------------------------------------
|
|
void
|
|
CUIThread::OnDescribeLocation(WPARAM uParam, LPARAM lParam) {
|
|
CSetLocationDescription *pSPD;
|
|
CFolder *pFolder;
|
|
CComponentDataImpl *pCDI;
|
|
CWnd cwndParent;
|
|
|
|
LPTSTR szDesc;
|
|
|
|
pFolder = reinterpret_cast<CFolder*>(uParam);
|
|
pCDI = reinterpret_cast<CComponentDataImpl*>(lParam);
|
|
LONG_PTR dwKey = DLG_KEY(pFolder, CSetLocationDescription::IDD);
|
|
|
|
|
|
//
|
|
// Look for a preexisting version of this dialog in pCDI's cache.
|
|
// If it's not there then create a new one and add it.
|
|
//
|
|
pSPD = (CSetLocationDescription *)pCDI->GetPopupDialog( dwKey );
|
|
if (NULL == pSPD) {
|
|
pSPD = new CSetLocationDescription;
|
|
if (NULL == pSPD) {
|
|
return;
|
|
}
|
|
pCDI->AddPopupDialog(dwKey ,pSPD);
|
|
}
|
|
|
|
// pSPD->Initialize(pFolder,pCDI);
|
|
|
|
|
|
if (!pSPD->GetSafeHwnd())
|
|
{
|
|
pSPD->Initialize(pFolder,pCDI);
|
|
|
|
cwndParent.Attach(pCDI->GetParentWindow());
|
|
CThemeContextActivator activator;
|
|
pSPD->Create(IDD_SET_DESCRIPTION,&cwndParent);
|
|
pSPD->SetWindowText(pFolder->GetName());
|
|
cwndParent.Detach();
|
|
}
|
|
|
|
pSPD->UpdateData(FALSE);
|
|
pSPD->BringWindowToTop();
|
|
pSPD->ShowWindow(SW_SHOWNORMAL);
|
|
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------
|
|
Method: OnDestroyDialog
|
|
|
|
Synopsis: Destroys and deletes the CAttribute object associated with [pDlg]
|
|
|
|
Arguments: [pDlg] - Is a pointer to the object to delete.
|
|
|
|
Histroy:
|
|
-----------------------------------------------------------------------------------*/
|
|
void CUIThread::OnDestroyDialog(WPARAM pDlg, LPARAM)
|
|
{
|
|
if(pDlg){
|
|
CAttribute *pAttr = reinterpret_cast<CAttribute *>(pDlg);
|
|
|
|
delete pAttr;
|
|
}
|
|
}
|
|
|
|
|
|
//+--------------------------------------------------------------------------
|
|
//
|
|
// Method: OnNewConfiguration
|
|
//
|
|
// Synopsis: Create and display a dialog for adding a new configuration file
|
|
//
|
|
// Arguments: [uParam] - A pointer to the CFolder parent of the new config file
|
|
// [lParam] - A pointer to the CComponentDataItem
|
|
//
|
|
//---------------------------------------------------------------------------
|
|
void
|
|
CUIThread::OnNewConfiguration(WPARAM uParam, LPARAM lParam)
|
|
{
|
|
CNewProfile *pNP;
|
|
CFolder *pFolder;
|
|
CComponentDataImpl *pCDI;
|
|
CWnd cwndParent;
|
|
|
|
LPTSTR szDesc;
|
|
|
|
pFolder = reinterpret_cast<CFolder*>(uParam);
|
|
pCDI = reinterpret_cast<CComponentDataImpl*>(lParam);
|
|
LONG_PTR dwKey = DLG_KEY(pFolder, CNewProfile::IDD);
|
|
|
|
|
|
//
|
|
// Look for a preexisting version of this dialog in pCDI's cache.
|
|
// If it's not there then create a new one and add it.
|
|
//
|
|
pNP = (CNewProfile *)pCDI->GetPopupDialog( dwKey);
|
|
if (NULL == pNP) {
|
|
pNP = new CNewProfile;
|
|
if (NULL == pNP) {
|
|
return;
|
|
}
|
|
pCDI->AddPopupDialog( dwKey, pNP);
|
|
}
|
|
|
|
// pNP->Initialize(pFolder,pCDI);
|
|
|
|
|
|
if (!pNP->GetSafeHwnd()) {
|
|
pNP->Initialize(pFolder,pCDI);
|
|
|
|
cwndParent.Attach(pCDI->GetParentWindow());
|
|
|
|
CThemeContextActivator activator;
|
|
pNP->Create(IDD_NEW_PROFILE,&cwndParent);
|
|
pNP->SetWindowText(pFolder->GetName());
|
|
cwndParent.Detach();
|
|
}
|
|
|
|
pNP->UpdateData(FALSE);
|
|
pNP->BringWindowToTop();
|
|
pNP->ShowWindow(SW_SHOWNORMAL);
|
|
|
|
}
|
|
|
|
//+--------------------------------------------------------------------------
|
|
// Method: OnAddPropsheet
|
|
//
|
|
// Synopsis: Adds a property sheet to the list of sheets to which to pass messages
|
|
// in PreTranslateMessage
|
|
//
|
|
// Arguments: [wParam] - HWND of the added property sheet
|
|
// [lParam] - unused
|
|
//
|
|
//---------------------------------------------------------------------------
|
|
void CUIThread::OnAddPropSheet(WPARAM wParam, LPARAM lParam)
|
|
{
|
|
if (IsWindow((HWND)wParam)) {
|
|
m_PSHwnds.AddHead((HWND)wParam);
|
|
}
|
|
}
|
|
|
|
|
|
BOOL CUIThread::PreTranslateMessage(MSG* pMsg)
|
|
{
|
|
//
|
|
// check PropSheet_GetCurrentPageHwnd to see if we need to destroy
|
|
// one of our modeless property sheets
|
|
//
|
|
|
|
POSITION pos;
|
|
POSITION posCur;
|
|
HWND hwnd;
|
|
|
|
pos= m_PSHwnds.GetHeadPosition();
|
|
while (pos) {
|
|
posCur = pos;
|
|
hwnd = m_PSHwnds.GetNext(pos);
|
|
|
|
if (!IsWindow(hwnd)) {
|
|
m_PSHwnds.RemoveAt(posCur);
|
|
} else if (NULL == PropSheet_GetCurrentPageHwnd(hwnd)) {
|
|
//
|
|
// hwnd is a closed property sheet. destroy it and remove it from the list
|
|
//
|
|
DestroyWindow(hwnd);
|
|
m_PSHwnds.RemoveAt(posCur);
|
|
}
|
|
|
|
if (PropSheet_IsDialogMessage(hwnd,pMsg)) {
|
|
//
|
|
// Message has been handled, so don't do anything else with it
|
|
//
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
return CWinThread::PreTranslateMessage(pMsg);
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
// CModelessDlgUIThread implementation
|
|
|
|
IMPLEMENT_DYNCREATE(CModelessDlgUIThread, CUIThread)
|
|
|
|
//------------------------------------------------------------------------------
|
|
//------------------------------------------------------------------------------
|
|
CModelessDlgUIThread::CModelessDlgUIThread()
|
|
{
|
|
m_hReadyForMsg = ::CreateEvent(NULL, TRUE, FALSE, NULL);
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
//------------------------------------------------------------------------------
|
|
CModelessDlgUIThread::~CModelessDlgUIThread()
|
|
{
|
|
if (NULL != m_hReadyForMsg)
|
|
::CloseHandle(m_hReadyForMsg);
|
|
}
|
|
|
|
BEGIN_MESSAGE_MAP(CModelessDlgUIThread, CUIThread)
|
|
//{{AFX_MSG_MAP(CModelessDlgUIThread)
|
|
// NOTE - the ClassWizard will add and remove mapping macros here.
|
|
//}}AFX_MSG_MAP
|
|
ON_THREAD_MESSAGE( SCEM_CREATE_MODELESS_SHEET, OnCreateModelessSheet)
|
|
ON_THREAD_MESSAGE( SCEM_DESTROY_WINDOW, OnDestroyWindow)
|
|
END_MESSAGE_MAP()
|
|
|
|
//------------------------------------------------------------------------------
|
|
//------------------------------------------------------------------------------
|
|
int CModelessDlgUIThread::Run()
|
|
{
|
|
if (m_hReadyForMsg)
|
|
::SetEvent(m_hReadyForMsg);
|
|
|
|
return CWinThread::Run();
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// wparam is PMLSHEET_DATA, and lparam is not used and can be used in the future
|
|
// for telling what property sheet is to be created (right now, it only works
|
|
// for ACL editor)
|
|
//------------------------------------------------------------------------------
|
|
void CModelessDlgUIThread::OnCreateModelessSheet(WPARAM wparam, LPARAM lparam)
|
|
{
|
|
PMLSHEET_DATA pSheetData = (PMLSHEET_DATA)wparam;
|
|
if (pSheetData)
|
|
{
|
|
HWND hSheet = (HWND)MyCreateSecurityPage2(pSheetData->bIsContainer,
|
|
pSheetData->ppSeDescriptor,
|
|
pSheetData->pSeInfo,
|
|
pSheetData->strObjectName,
|
|
pSheetData->SeType,
|
|
pSheetData->flag,
|
|
pSheetData->hwndParent,
|
|
TRUE);
|
|
*(pSheetData->phwndSheet) = hSheet;
|
|
|
|
OnAddPropSheet((WPARAM)hSheet, 0);
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// wparam is the window handle and lparam is not used at this time
|
|
// Since destroying a window must happen on the thread where it is contructed,
|
|
// this is necessary. Basic window management rules must be followed, e.g.,
|
|
// don't ask this thread to destroy windows that is not created by it
|
|
//------------------------------------------------------------------------------
|
|
void CModelessDlgUIThread::OnDestroyWindow(WPARAM wparam, LPARAM lparam)
|
|
{
|
|
if (::IsWindow((HWND)wparam))
|
|
{
|
|
DestroyWindow((HWND)wparam);
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// immediately after this thread object is created, creating thread needs to
|
|
// wait by calling this function so that the newly created thread has a chance
|
|
// to be scheduled to run.
|
|
//------------------------------------------------------------------------------
|
|
void CModelessDlgUIThread::WaitTillRun()
|
|
{
|
|
if (NULL != m_hReadyForMsg)
|
|
{
|
|
// $UNDONE:shawnwu I found that MMC runs at THREAD_PRIORITY_ABOVE_NORMAL (or higher) priority
|
|
// becaues only when I run this UI thread at THREAD_PRIORITY_HIGHEST will this thread schedule to run.
|
|
// But running at that level of priority makes me feel a little bit nervous. I thus
|
|
// leave this sleep(10) code here. Up to the UI team to decide.
|
|
::Sleep(10);
|
|
::WaitForSingleObject(m_hReadyForMsg, INFINITE); // don't care about result of this waiting
|
|
// now the event is useless. To reduce resource overhead, close the handle
|
|
::CloseHandle(m_hReadyForMsg);
|
|
m_hReadyForMsg = NULL;
|
|
}
|
|
}
|
|
|