2015-06-17 21:26:42 +00:00
|
|
|
/*
|
|
|
|
* PROJECT: ReactOS Device Manager
|
|
|
|
* LICENSE: GPL - See COPYING in the top level directory
|
2015-10-15 17:21:37 +00:00
|
|
|
* FILE: dll/win32/devmgr/devmgmt/ClassNode.cpp
|
2015-06-17 21:26:42 +00:00
|
|
|
* PURPOSE: Class object for
|
|
|
|
* COPYRIGHT: Copyright 2015 Ged Murphy <gedmurphy@reactos.org>
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
2015-10-15 17:21:37 +00:00
|
|
|
#include "precomp.h"
|
2015-06-17 21:26:42 +00:00
|
|
|
#include "devmgmt.h"
|
|
|
|
#include "DeviceNode.h"
|
|
|
|
|
|
|
|
|
|
|
|
CDeviceNode::CDeviceNode(
|
|
|
|
_In_opt_ DEVINST Device,
|
|
|
|
_In_ PSP_CLASSIMAGELIST_DATA ImageListData
|
|
|
|
) :
|
2015-07-03 06:56:58 +00:00
|
|
|
CNode(DeviceNode, ImageListData),
|
2015-06-17 21:26:42 +00:00
|
|
|
m_DevInst(Device),
|
2015-06-26 08:45:49 +00:00
|
|
|
m_hDevInfo(NULL),
|
2015-06-17 21:26:42 +00:00
|
|
|
m_Status(0),
|
|
|
|
m_ProblemNumber(0),
|
|
|
|
m_OverlayImage(0)
|
|
|
|
{
|
2015-06-25 18:59:23 +00:00
|
|
|
ZeroMemory(&m_DevinfoData, sizeof(SP_DEVINFO_DATA));
|
2015-06-17 21:26:42 +00:00
|
|
|
}
|
|
|
|
|
2015-10-23 12:45:41 +00:00
|
|
|
CDeviceNode::CDeviceNode(
|
|
|
|
_In_ const CDeviceNode &Node
|
|
|
|
) :
|
|
|
|
CNode(Node)
|
|
|
|
{
|
|
|
|
m_DevInst = Node.m_DevInst;
|
|
|
|
m_hDevInfo = Node.m_hDevInfo;
|
|
|
|
m_Status = Node.m_Status;
|
|
|
|
m_ProblemNumber = Node.m_ProblemNumber;
|
|
|
|
m_OverlayImage = Node.m_OverlayImage;
|
|
|
|
CopyMemory(&m_DevinfoData, &Node.m_DevinfoData, sizeof(SP_DEVINFO_DATA));
|
|
|
|
|
|
|
|
size_t size = wcslen(Node.m_DeviceId) + 1;
|
|
|
|
m_DeviceId = new WCHAR[size];
|
|
|
|
StringCbCopyW(m_DeviceId, size * sizeof(WCHAR), Node.m_DeviceId);
|
|
|
|
}
|
|
|
|
|
2015-06-17 21:26:42 +00:00
|
|
|
CDeviceNode::~CDeviceNode()
|
|
|
|
{
|
2015-06-26 08:45:49 +00:00
|
|
|
Cleanup();
|
2015-06-17 21:26:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
CDeviceNode::SetupNode()
|
|
|
|
{
|
|
|
|
WCHAR ClassGuidString[MAX_GUID_STRING_LEN];
|
|
|
|
ULONG ulLength;
|
|
|
|
CONFIGRET cr;
|
|
|
|
|
|
|
|
// Get the length of the device id string
|
|
|
|
cr = CM_Get_Device_ID_Size(&ulLength, m_DevInst, 0);
|
|
|
|
if (cr == CR_SUCCESS)
|
|
|
|
{
|
|
|
|
// We alloc heap here because this will be stored in the lParam of the TV
|
2015-06-26 08:45:49 +00:00
|
|
|
m_DeviceId = new WCHAR[ulLength + 1];
|
|
|
|
|
|
|
|
// Now get the actual device id
|
|
|
|
cr = CM_Get_Device_IDW(m_DevInst,
|
2017-02-19 19:40:04 +00:00
|
|
|
m_DeviceId,
|
|
|
|
ulLength + 1,
|
|
|
|
0);
|
2017-02-19 19:13:41 +00:00
|
|
|
if (cr != CR_SUCCESS || wcscmp(m_DeviceId, L"HTREE\\ROOT\\0") == 0)
|
2015-06-17 21:26:42 +00:00
|
|
|
{
|
2015-06-26 08:45:49 +00:00
|
|
|
delete[] m_DeviceId;
|
|
|
|
m_DeviceId = NULL;
|
2015-06-17 21:26:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure we got the string
|
|
|
|
if (m_DeviceId == NULL)
|
|
|
|
return false;
|
|
|
|
|
2015-06-26 08:45:49 +00:00
|
|
|
// Build up a handle a and devinfodata struct
|
2015-06-25 18:59:23 +00:00
|
|
|
m_hDevInfo = SetupDiCreateDeviceInfoListExW(NULL,
|
|
|
|
NULL,
|
|
|
|
NULL,
|
|
|
|
NULL);
|
|
|
|
if (m_hDevInfo != INVALID_HANDLE_VALUE)
|
|
|
|
{
|
|
|
|
m_DevinfoData.cbSize = sizeof(SP_DEVINFO_DATA);
|
|
|
|
SetupDiOpenDeviceInfoW(m_hDevInfo,
|
|
|
|
m_DeviceId,
|
|
|
|
NULL,
|
|
|
|
0,
|
|
|
|
&m_DevinfoData);
|
|
|
|
}
|
|
|
|
|
2015-10-25 17:01:19 +00:00
|
|
|
// Check if the device has a problem
|
2015-06-26 08:45:49 +00:00
|
|
|
if (HasProblem())
|
2015-06-17 21:26:42 +00:00
|
|
|
{
|
2015-10-25 17:01:19 +00:00
|
|
|
if (IsDisabled())
|
|
|
|
{
|
|
|
|
m_OverlayImage = OverlayDisabled;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
m_OverlayImage = OverlayProblem;
|
|
|
|
}
|
2015-06-17 21:26:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get the class guid for this device
|
|
|
|
ulLength = MAX_GUID_STRING_LEN * sizeof(WCHAR);
|
|
|
|
cr = CM_Get_DevNode_Registry_PropertyW(m_DevInst,
|
|
|
|
CM_DRP_CLASSGUID,
|
|
|
|
NULL,
|
|
|
|
ClassGuidString,
|
|
|
|
&ulLength,
|
|
|
|
0);
|
|
|
|
if (cr == CR_SUCCESS)
|
|
|
|
{
|
|
|
|
// Convert the string to a proper guid
|
|
|
|
CLSIDFromString(ClassGuidString, &m_ClassGuid);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// It's a device with no driver
|
|
|
|
m_ClassGuid = GUID_DEVCLASS_UNKNOWN;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the image for the class this device is in
|
|
|
|
SetupDiGetClassImageIndex(m_ImageListData,
|
|
|
|
&m_ClassGuid,
|
|
|
|
&m_ClassImage);
|
|
|
|
|
|
|
|
// Get the description for the device
|
|
|
|
ulLength = DISPLAY_NAME_LEN * sizeof(WCHAR);
|
|
|
|
cr = CM_Get_DevNode_Registry_PropertyW(m_DevInst,
|
|
|
|
CM_DRP_FRIENDLYNAME,
|
|
|
|
NULL,
|
|
|
|
m_DisplayName,
|
|
|
|
&ulLength,
|
|
|
|
0);
|
|
|
|
if (cr != CR_SUCCESS)
|
|
|
|
{
|
|
|
|
ulLength = DISPLAY_NAME_LEN * sizeof(WCHAR);
|
|
|
|
cr = CM_Get_DevNode_Registry_PropertyW(m_DevInst,
|
|
|
|
CM_DRP_DEVICEDESC,
|
|
|
|
NULL,
|
|
|
|
m_DisplayName,
|
|
|
|
&ulLength,
|
|
|
|
0);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cr != CR_SUCCESS)
|
|
|
|
{
|
2015-10-15 17:21:37 +00:00
|
|
|
CAtlStringW str;
|
|
|
|
if (str.LoadStringW(g_hThisInstance, IDS_UNKNOWNDEVICE))
|
|
|
|
StringCchCopyW(m_DisplayName, MAX_PATH, str.GetBuffer());
|
2015-06-17 21:26:42 +00:00
|
|
|
}
|
|
|
|
|
2015-06-26 08:45:49 +00:00
|
|
|
return true;
|
2015-06-17 21:26:42 +00:00
|
|
|
}
|
|
|
|
|
2015-06-25 18:59:23 +00:00
|
|
|
bool
|
|
|
|
CDeviceNode::HasProblem()
|
|
|
|
{
|
|
|
|
CONFIGRET cr;
|
|
|
|
cr = CM_Get_DevNode_Status_Ex(&m_Status,
|
|
|
|
&m_ProblemNumber,
|
|
|
|
m_DevInst,
|
|
|
|
0,
|
|
|
|
NULL);
|
|
|
|
if (cr == CR_SUCCESS)
|
|
|
|
{
|
|
|
|
return ((m_Status & (DN_HAS_PROBLEM | DN_PRIVATE_PROBLEM)) != 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
2015-06-17 21:26:42 +00:00
|
|
|
|
|
|
|
bool
|
|
|
|
CDeviceNode::IsHidden()
|
|
|
|
{
|
|
|
|
CONFIGRET cr;
|
|
|
|
cr = CM_Get_DevNode_Status_Ex(&m_Status,
|
|
|
|
&m_ProblemNumber,
|
|
|
|
m_DevInst,
|
|
|
|
0,
|
|
|
|
NULL);
|
|
|
|
if (cr == CR_SUCCESS)
|
|
|
|
{
|
2015-10-26 08:40:14 +00:00
|
|
|
if (m_Status & DN_NO_SHOW_IN_DM)
|
|
|
|
return true;
|
2015-06-17 21:26:42 +00:00
|
|
|
}
|
|
|
|
|
2015-10-26 08:40:14 +00:00
|
|
|
if (IsEqualGUID(*GetClassGuid(), GUID_DEVCLASS_LEGACYDRIVER) ||
|
|
|
|
IsEqualGUID(*GetClassGuid(), GUID_DEVCLASS_VOLUME))
|
|
|
|
return true;
|
|
|
|
|
2015-06-17 21:26:42 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
CDeviceNode::CanDisable()
|
|
|
|
{
|
|
|
|
CONFIGRET cr;
|
|
|
|
cr = CM_Get_DevNode_Status_Ex(&m_Status,
|
|
|
|
&m_ProblemNumber,
|
|
|
|
m_DevInst,
|
|
|
|
0,
|
|
|
|
NULL);
|
|
|
|
if (cr == CR_SUCCESS)
|
|
|
|
{
|
|
|
|
return ((m_Status & DN_DISABLEABLE) != 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
CDeviceNode::IsDisabled()
|
|
|
|
{
|
|
|
|
CONFIGRET cr;
|
|
|
|
cr = CM_Get_DevNode_Status_Ex(&m_Status,
|
|
|
|
&m_ProblemNumber,
|
|
|
|
m_DevInst,
|
|
|
|
0,
|
|
|
|
NULL);
|
|
|
|
if (cr == CR_SUCCESS)
|
|
|
|
{
|
2015-10-25 17:01:19 +00:00
|
|
|
return ((m_ProblemNumber == CM_PROB_DISABLED) || (m_ProblemNumber == CM_PROB_HARDWARE_DISABLED));
|
2015-06-17 21:26:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
CDeviceNode::IsStarted()
|
|
|
|
{
|
|
|
|
CONFIGRET cr;
|
|
|
|
cr = CM_Get_DevNode_Status_Ex(&m_Status,
|
|
|
|
&m_ProblemNumber,
|
|
|
|
m_DevInst,
|
|
|
|
0,
|
|
|
|
NULL);
|
|
|
|
if (cr == CR_SUCCESS)
|
|
|
|
{
|
|
|
|
return ((m_Status & DN_STARTED) != 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
CDeviceNode::IsInstalled()
|
|
|
|
{
|
|
|
|
CONFIGRET cr;
|
|
|
|
cr = CM_Get_DevNode_Status_Ex(&m_Status,
|
|
|
|
&m_ProblemNumber,
|
|
|
|
m_DevInst,
|
|
|
|
0,
|
|
|
|
NULL);
|
|
|
|
if (cr == CR_SUCCESS)
|
|
|
|
{
|
|
|
|
return ((m_Status & DN_HAS_PROBLEM) != 0 ||
|
|
|
|
(m_Status & (DN_DRIVER_LOADED | DN_STARTED)) != 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
2015-06-25 18:59:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
CDeviceNode::CanUninstall()
|
|
|
|
{
|
|
|
|
CONFIGRET cr;
|
|
|
|
cr = CM_Get_DevNode_Status_Ex(&m_Status,
|
|
|
|
&m_ProblemNumber,
|
|
|
|
m_DevInst,
|
|
|
|
0,
|
|
|
|
NULL);
|
|
|
|
if (cr == CR_SUCCESS)
|
|
|
|
{
|
2015-07-03 06:56:58 +00:00
|
|
|
if ((m_Status & DN_ROOT_ENUMERATED) != 0 &&
|
|
|
|
(m_Status & DN_DISABLEABLE) == 0)
|
|
|
|
return false;
|
2015-06-25 18:59:23 +00:00
|
|
|
}
|
|
|
|
|
2015-07-03 06:56:58 +00:00
|
|
|
return true;
|
2015-06-25 18:59:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
CDeviceNode::EnableDevice(
|
|
|
|
_In_ bool Enable,
|
|
|
|
_Out_ bool &NeedsReboot
|
|
|
|
)
|
|
|
|
{
|
|
|
|
bool Canceled = false;
|
|
|
|
|
|
|
|
SetFlags(DI_NODI_DEFAULTACTION, 0);
|
|
|
|
|
|
|
|
SP_PROPCHANGE_PARAMS pcp;
|
|
|
|
pcp.ClassInstallHeader.cbSize = sizeof(SP_CLASSINSTALL_HEADER);
|
|
|
|
pcp.ClassInstallHeader.InstallFunction = DIF_PROPERTYCHANGE;
|
|
|
|
pcp.StateChange = (Enable ? DICS_ENABLE : DICS_DISABLE);
|
|
|
|
pcp.HwProfile = 0;
|
|
|
|
|
|
|
|
// check both scopes to make sure we can make the change
|
|
|
|
for (int i = 0; i < 2; i++)
|
|
|
|
{
|
|
|
|
// Check globally first, then check config specific
|
2015-10-23 12:45:41 +00:00
|
|
|
pcp.Scope = (i == 0) ? DICS_FLAG_CONFIGGENERAL : DICS_FLAG_CONFIGSPECIFIC;
|
2015-06-25 18:59:23 +00:00
|
|
|
|
|
|
|
if (SetupDiSetClassInstallParamsW(m_hDevInfo,
|
|
|
|
&m_DevinfoData,
|
|
|
|
&pcp.ClassInstallHeader,
|
|
|
|
sizeof(SP_PROPCHANGE_PARAMS)))
|
|
|
|
{
|
|
|
|
SetupDiCallClassInstaller(DIF_PROPERTYCHANGE,
|
|
|
|
m_hDevInfo,
|
|
|
|
&m_DevinfoData);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (GetLastError() == ERROR_CANCELLED)
|
|
|
|
{
|
|
|
|
Canceled = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Canceled == false)
|
|
|
|
{
|
|
|
|
pcp.Scope = DICS_FLAG_CONFIGSPECIFIC;
|
|
|
|
if (SetupDiSetClassInstallParamsW(m_hDevInfo,
|
|
|
|
&m_DevinfoData,
|
|
|
|
&pcp.ClassInstallHeader,
|
|
|
|
sizeof(SP_PROPCHANGE_PARAMS)))
|
|
|
|
{
|
|
|
|
SetupDiChangeState(m_hDevInfo, &m_DevinfoData);
|
|
|
|
}
|
|
|
|
|
2015-10-23 12:45:41 +00:00
|
|
|
|
2015-06-25 18:59:23 +00:00
|
|
|
if (Enable)
|
|
|
|
{
|
2015-06-26 08:45:49 +00:00
|
|
|
// config specific enabling first, then global enabling.
|
2015-06-25 20:34:57 +00:00
|
|
|
// The global appears to be the one that starts the device
|
2015-06-25 18:59:23 +00:00
|
|
|
pcp.Scope = DICS_FLAG_GLOBAL;
|
|
|
|
if (SetupDiSetClassInstallParamsW(m_hDevInfo,
|
|
|
|
&m_DevinfoData,
|
|
|
|
&pcp.ClassInstallHeader,
|
|
|
|
sizeof(SP_PROPCHANGE_PARAMS)))
|
|
|
|
{
|
|
|
|
SetupDiChangeState(m_hDevInfo, &m_DevinfoData);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
SetFlags(DI_PROPERTIES_CHANGE, 0);
|
|
|
|
|
|
|
|
NeedsReboot = ((GetFlags() & (DI_NEEDRESTART | DI_NEEDREBOOT)) != 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
RemoveFlags(DI_NODI_DEFAULTACTION, 0);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-07-05 08:49:54 +00:00
|
|
|
bool
|
|
|
|
CDeviceNode::UninstallDevice()
|
|
|
|
{
|
|
|
|
if (CanUninstall() == false)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
SP_REMOVEDEVICE_PARAMS RemoveDevParams;
|
|
|
|
RemoveDevParams.ClassInstallHeader.cbSize = sizeof(SP_CLASSINSTALL_HEADER);
|
|
|
|
RemoveDevParams.ClassInstallHeader.InstallFunction = DIF_REMOVE;
|
|
|
|
RemoveDevParams.Scope = DI_REMOVEDEVICE_GLOBAL;
|
|
|
|
RemoveDevParams.HwProfile = 0;
|
|
|
|
|
|
|
|
//
|
|
|
|
// We probably need to walk all the siblings of this
|
|
|
|
// device and ask if they're happy with the uninstall
|
|
|
|
//
|
|
|
|
|
|
|
|
// Remove it
|
|
|
|
SetupDiSetClassInstallParamsW(m_hDevInfo,
|
|
|
|
&m_DevinfoData,
|
|
|
|
&RemoveDevParams.ClassInstallHeader,
|
|
|
|
sizeof(SP_REMOVEDEVICE_PARAMS));
|
|
|
|
SetupDiCallClassInstaller(DIF_REMOVE, m_hDevInfo, &m_DevinfoData);
|
|
|
|
|
|
|
|
// Clear the install params
|
|
|
|
SetupDiSetClassInstallParamsW(m_hDevInfo,
|
|
|
|
&m_DevinfoData,
|
|
|
|
NULL,
|
|
|
|
0);
|
|
|
|
|
2015-07-07 11:05:25 +00:00
|
|
|
return true;
|
2015-07-05 08:49:54 +00:00
|
|
|
}
|
|
|
|
|
2015-06-26 08:45:49 +00:00
|
|
|
/* PRIVATE METHODS ******************************************************/
|
|
|
|
|
|
|
|
void
|
|
|
|
CDeviceNode::Cleanup()
|
|
|
|
{
|
|
|
|
if (m_DeviceId)
|
|
|
|
{
|
|
|
|
delete[] m_DeviceId;
|
|
|
|
m_DeviceId = NULL;
|
|
|
|
}
|
|
|
|
if (m_hDevInfo)
|
|
|
|
{
|
|
|
|
SetupDiDestroyDeviceInfoList(m_hDevInfo);
|
|
|
|
m_hDevInfo = NULL;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-25 18:59:23 +00:00
|
|
|
DWORD
|
2017-02-19 19:40:04 +00:00
|
|
|
CDeviceNode::GetFlags()
|
2015-06-25 18:59:23 +00:00
|
|
|
{
|
|
|
|
SP_DEVINSTALL_PARAMS DevInstallParams;
|
|
|
|
DevInstallParams.cbSize = sizeof(SP_DEVINSTALL_PARAMS);
|
|
|
|
if (SetupDiGetDeviceInstallParamsW(m_hDevInfo,
|
|
|
|
&m_DevinfoData,
|
|
|
|
&DevInstallParams))
|
|
|
|
{
|
|
|
|
return DevInstallParams.Flags;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
CDeviceNode::SetFlags(
|
|
|
|
_In_ DWORD Flags,
|
|
|
|
_In_ DWORD FlagsEx
|
|
|
|
)
|
|
|
|
{
|
|
|
|
SP_DEVINSTALL_PARAMS DevInstallParams;
|
|
|
|
DevInstallParams.cbSize = sizeof(SP_DEVINSTALL_PARAMS);
|
|
|
|
if (SetupDiGetDeviceInstallParamsW(m_hDevInfo,
|
|
|
|
&m_DevinfoData,
|
|
|
|
&DevInstallParams))
|
|
|
|
{
|
|
|
|
DevInstallParams.Flags |= Flags;
|
|
|
|
DevInstallParams.FlagsEx |= FlagsEx;
|
2015-07-07 11:05:25 +00:00
|
|
|
return (SetupDiSetDeviceInstallParamsW(m_hDevInfo,
|
|
|
|
&m_DevinfoData,
|
|
|
|
&DevInstallParams) != 0);
|
2015-06-25 18:59:23 +00:00
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
CDeviceNode::RemoveFlags(
|
|
|
|
_In_ DWORD Flags,
|
|
|
|
_In_ DWORD FlagsEx
|
|
|
|
)
|
|
|
|
{
|
|
|
|
SP_DEVINSTALL_PARAMS DevInstallParams;
|
|
|
|
DevInstallParams.cbSize = sizeof(SP_DEVINSTALL_PARAMS);
|
|
|
|
if (SetupDiGetDeviceInstallParamsW(m_hDevInfo,
|
|
|
|
&m_DevinfoData,
|
|
|
|
&DevInstallParams))
|
|
|
|
{
|
|
|
|
DevInstallParams.Flags &= ~Flags;
|
|
|
|
DevInstallParams.FlagsEx &= ~FlagsEx;
|
2015-07-07 11:05:25 +00:00
|
|
|
return (SetupDiSetDeviceInstallParamsW(m_hDevInfo,
|
|
|
|
&m_DevinfoData,
|
|
|
|
&DevInstallParams) != 0);
|
2015-06-25 18:59:23 +00:00
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|