mirror of
https://github.com/spiffcode/hostile-takeover.git
synced 2026-03-28 15:59:40 -06:00
616 lines
14 KiB
C++
616 lines
14 KiB
C++
#include "..\ht.h"
|
|
#include "..\Multiplayer.h"
|
|
#include <BtSerTransport.h>
|
|
#include <TCHAR.H>
|
|
#ifndef CE
|
|
#include <setupapi.h>
|
|
#include <devguid.h>
|
|
#include <WinIoCtl.h>
|
|
#endif
|
|
|
|
// The 'inbound' port must be used to 'Advertise' availability of this device
|
|
// for others to connect to it. The 'outbound' port, when used, will invoke
|
|
// "Bluetooth Browser" UI for connecting to another device.
|
|
|
|
int BtSerTransport::GetTransportDescriptions(TransportDescription *atrad, int ctradMax)
|
|
{
|
|
// Make sure the BT com port is there and available
|
|
|
|
word wInboundPort = 0xffff, wOutboundPort = 0xffff;
|
|
|
|
#ifdef CE
|
|
HKEY hkey;
|
|
if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, TEXT("Drivers\\BuiltIn"), 0, KEY_ALL_ACCESS, &hkey) != ERROR_SUCCESS)
|
|
return 0;
|
|
|
|
int i = 0;
|
|
while (true) {
|
|
TCHAR tszT[100];
|
|
FILETIME ftT;
|
|
DWORD cbtszT = sizeof(tszT);
|
|
if (RegEnumKeyEx(hkey, i++, (TCHAR *)&tszT, &cbtszT, NULL, NULL, NULL, &ftT) != ERROR_SUCCESS)
|
|
break;
|
|
|
|
HKEY hkeyDevice;
|
|
if (RegOpenKeyEx(hkey, tszT, 0, KEY_ALL_ACCESS, &hkeyDevice) != ERROR_SUCCESS)
|
|
break;
|
|
|
|
// Look for devices with "COM" prefixes
|
|
|
|
TCHAR tszPrefix[50];
|
|
DWORD cbT = sizeof(tszPrefix);
|
|
if (RegQueryValueEx(hkeyDevice, TEXT("Prefix"), 0, NULL, (byte *)tszPrefix, &cbT) != ERROR_SUCCESS) {
|
|
RegCloseKey(hkeyDevice);
|
|
continue;
|
|
}
|
|
if (_tcscmp(tszPrefix, TEXT("COM")) != 0) {
|
|
RegCloseKey(hkeyDevice);
|
|
continue;
|
|
}
|
|
|
|
// Get this COM device's index
|
|
|
|
DWORD dwIndex;
|
|
cbT = sizeof(DWORD);
|
|
if (RegQueryValueEx(hkeyDevice, TEXT("Index"), 0, NULL, (byte *)&dwIndex, &cbT) != ERROR_SUCCESS) {
|
|
RegCloseKey(hkeyDevice);
|
|
continue;
|
|
}
|
|
|
|
#if 0 // don't do this because it can make UI ("Bluetooth Browser") pop up
|
|
// See if we can open this port
|
|
|
|
TCHAR tszComPort[20];
|
|
_stprintf(tszComPort, TEXT("COM%d:"), dwIndex);
|
|
HANDLE hf = CreateFile(tszComPort, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
|
|
if (hf == INVALID_HANDLE_VALUE) {
|
|
RegCloseKey(hkeyDevice);
|
|
continue;
|
|
}
|
|
CloseHandle(hf);
|
|
#endif
|
|
|
|
// Get the device's friendly name
|
|
|
|
TCHAR tszFriendly[100];
|
|
cbT = sizeof(tszFriendly);
|
|
if (RegQueryValueEx(hkeyDevice, TEXT("FriendlyName"), 0, NULL, (byte *)tszFriendly, &cbT) != ERROR_SUCCESS) {
|
|
// sprintf(ptrad->szName, "COM%d", dwIndex);
|
|
RegCloseKey(hkeyDevice);
|
|
continue;
|
|
|
|
}
|
|
|
|
RegCloseKey(hkeyDevice);
|
|
|
|
// Is this a Bluetooth port
|
|
// UNDONE: friendly names are localized?
|
|
|
|
_tcsupr(tszFriendly);
|
|
if (_tcsstr(tszFriendly, TEXT("BLUETOOTH")) != NULL) {
|
|
|
|
// Assign first Bluetooth port as the inbound port, second as the outbound port
|
|
|
|
if (wInboundPort == 0xffff)
|
|
wInboundPort = (word)dwIndex;
|
|
else if (wOutboundPort == 0xffff)
|
|
wOutboundPort = (word)dwIndex;
|
|
}
|
|
}
|
|
|
|
RegCloseKey(hkey);
|
|
|
|
#else
|
|
HDEVINFO hdevi = INVALID_HANDLE_VALUE;
|
|
SP_DEVICE_INTERFACE_DETAIL_DATA *pDetData = NULL;
|
|
|
|
// Let's take a look at com ports
|
|
|
|
hdevi = SetupDiGetClassDevs(&GUID_CLASS_COMPORT, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
|
|
|
|
if (hdevi == INVALID_HANDLE_VALUE) {
|
|
Assert("SetupDiGetClassDevs failed. (err=%lx)", GetLastError());
|
|
return 0;
|
|
}
|
|
|
|
// Enumerate the serial ports
|
|
|
|
SP_DEVICE_INTERFACE_DATA difd;
|
|
DWORD dwDetDataSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA) + 256;
|
|
pDetData = (SP_DEVICE_INTERFACE_DETAIL_DATA*) new char[dwDetDataSize];
|
|
|
|
// This is required, according to the documentation. Yes, it's weird.
|
|
|
|
difd.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
|
|
pDetData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
|
|
for (int i = 0; true; i++) {
|
|
if (!SetupDiEnumDeviceInterfaces(hdevi, NULL, &GUID_CLASS_COMPORT, i, &difd)) {
|
|
DWORD err = GetLastError();
|
|
if (err != ERROR_NO_MORE_ITEMS)
|
|
Assert("SetupDiEnumDeviceInterfaces failed. (err=%lx)", err);
|
|
break;
|
|
}
|
|
|
|
SP_DEVINFO_DATA devdata = { sizeof(SP_DEVINFO_DATA) };
|
|
if (!SetupDiGetDeviceInterfaceDetail(hdevi, &difd, pDetData, dwDetDataSize, NULL, &devdata)) {
|
|
Assert("SetupDiGetDeviceInterfaceDetail failed. (err=%lx)", GetLastError());
|
|
break;
|
|
}
|
|
|
|
// Got a path to the device. Try to get some more info.
|
|
|
|
// TCHAR szFriendly[256];
|
|
// BOOL fSuccess = SetupDiGetDeviceRegistryProperty(hdevi, &devdata, SPDRP_FRIENDLYNAME, NULL, (PBYTE)szFriendly, sizeof(szFriendly), NULL);
|
|
TCHAR szDesc[256];
|
|
BOOL fSuccess = SetupDiGetDeviceRegistryProperty(hdevi, &devdata, SPDRP_DEVICEDESC, NULL, (PBYTE)szDesc, sizeof(szDesc), NULL);
|
|
if (!fSuccess)
|
|
continue;
|
|
|
|
// Is this a Bluetooth port?
|
|
|
|
strupr(szDesc);
|
|
if ((strstr(szDesc, "BLUETOOTH")) != NULL) {
|
|
|
|
// Get its PortName
|
|
|
|
HKEY hkey = SetupDiOpenDevRegKey(hdevi, &devdata, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_ALL_ACCESS);
|
|
if (hkey == NULL)
|
|
continue;
|
|
|
|
TCHAR szPortName[256];
|
|
DWORD cbT = sizeof(szPortName);
|
|
if (RegQueryValueEx(hkey, "PortName", 0, NULL, (byte *)szPortName, &cbT) != ERROR_SUCCESS) {
|
|
RegCloseKey(hkey);
|
|
continue;
|
|
}
|
|
|
|
RegCloseKey(hkey);
|
|
|
|
// Assign first Bluetooth port as the inbound port, second as the outbound port
|
|
|
|
if (wInboundPort == 0xffff)
|
|
wInboundPort = (word)(szPortName[3] - '0');
|
|
else if (wOutboundPort == 0xffff)
|
|
wOutboundPort = (word)(szPortName[3] - '0');
|
|
strupr(szPortName);
|
|
}
|
|
}
|
|
|
|
delete pDetData;
|
|
SetupDiDestroyDeviceInfoList(hdevi);
|
|
#endif
|
|
|
|
if (wInboundPort == 0xffff || wOutboundPort == 0xffff)
|
|
return 0;
|
|
atrad[0].dwTransportSpecific = ((DWORD)wInboundPort << 16) | wOutboundPort;
|
|
atrad[0].trat = ktratBluetoothSer;
|
|
strcpy(atrad[0].szName, "BLUETOOTH SERIAL");
|
|
atrad[0].pfnOpen = BtSerTransport::Open;
|
|
|
|
return 1;
|
|
}
|
|
|
|
Transport *BtSerTransport::Open(TransportDescription *ptrad)
|
|
{
|
|
Transport *ptra = new BtSerTransport(ptrad->dwTransportSpecific);
|
|
if (ptra == NULL)
|
|
return NULL;
|
|
|
|
if (!ptra->Open())
|
|
return NULL;
|
|
return ptra;
|
|
}
|
|
|
|
BtSerTransport::BtSerTransport(dword dwPorts)
|
|
{
|
|
m_wInboundPort = (word)(dwPorts >> 16);
|
|
m_wOutboundPort = (word)(dwPorts & 0xffff);
|
|
m_hfInbound = NULL;
|
|
m_hfOutbound = NULL;
|
|
m_cInboundPortRef = m_cOutboundPortRef = 0;
|
|
m_wf = 0;
|
|
}
|
|
|
|
bool BtSerTransport::Open()
|
|
{
|
|
// Nothing to do
|
|
|
|
return Transport::Open();
|
|
}
|
|
|
|
void BtSerTransport::Close()
|
|
{
|
|
Transport::Close(); // Closes any open Connections
|
|
}
|
|
|
|
HANDLE BtSerTransport::OpenInboundPort()
|
|
{
|
|
if (m_hfInbound != NULL) {
|
|
m_cInboundPortRef++;
|
|
return m_hfInbound;
|
|
}
|
|
|
|
TCHAR tszPort[10];
|
|
_stprintf(tszPort, TEXT("COM%d:"), m_wInboundPort);
|
|
m_hfInbound = CreateFile(tszPort, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
|
|
if (m_hfInbound == INVALID_HANDLE_VALUE) {
|
|
HtMessageBox(kfMbWhiteBorder, "Communication Error", "Failed to open COM%d (%d).", m_wInboundPort, GetLastError());
|
|
return INVALID_HANDLE_VALUE;
|
|
}
|
|
|
|
m_cInboundPortRef = 1;
|
|
return m_hfInbound;
|
|
}
|
|
|
|
void BtSerTransport::CloseInboundPort()
|
|
{
|
|
Assert(m_cInboundPortRef > 0);
|
|
m_cInboundPortRef--;
|
|
if (m_cInboundPortRef == 0) {
|
|
CloseHandle(m_hfInbound);
|
|
m_hfInbound = NULL;
|
|
}
|
|
}
|
|
|
|
HANDLE BtSerTransport::OpenOutboundPort()
|
|
{
|
|
if (m_hfOutbound != NULL) {
|
|
m_cOutboundPortRef++;
|
|
return m_hfOutbound;
|
|
}
|
|
|
|
TCHAR tszPort[10];
|
|
_stprintf(tszPort, TEXT("COM%d:"), m_wOutboundPort);
|
|
m_hfOutbound = CreateFile(tszPort, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
|
|
if (m_hfOutbound == INVALID_HANDLE_VALUE) {
|
|
HtMessageBox(kfMbWhiteBorder, "Communication Error", "Failed to open COM%d (%d).", m_wOutboundPort, GetLastError());
|
|
return INVALID_HANDLE_VALUE;
|
|
}
|
|
|
|
m_cOutboundPortRef = 1;
|
|
return m_hfOutbound;
|
|
}
|
|
|
|
void BtSerTransport::CloseOutboundPort()
|
|
{
|
|
Assert(m_cOutboundPortRef > 0);
|
|
m_cOutboundPortRef--;
|
|
if (m_cOutboundPortRef == 0) {
|
|
CloseHandle(m_hfOutbound);
|
|
m_hfOutbound = NULL;
|
|
}
|
|
}
|
|
|
|
Connection *BtSerTransport::NewConnection()
|
|
{
|
|
return new BtSerConnection();
|
|
}
|
|
|
|
// BeginGameSearch is called when the JoinOrHostMultiplayer form enters its
|
|
// modal loop. EndGameSearch is called when the form is destructed.
|
|
|
|
// Serial GameSearch
|
|
// - create a non-blocking datagram socket to listen for SERVERINFO broadcasts
|
|
// - poll the socket to check for SERVERINFO broadcasts every 1/10th of a second
|
|
// - for each SERVERINFO broadcast received call the Transport's registered
|
|
// ITransportCallback::OnReceive method
|
|
|
|
bool BtSerTransport::BeginGameSearch()
|
|
{
|
|
// Already searching?
|
|
|
|
if ((m_wf & kfBstSearchingForGames) != 0)
|
|
return false;
|
|
|
|
if (OpenOutboundPort() == INVALID_HANDLE_VALUE)
|
|
return false;
|
|
|
|
// UNDONE: set a timeout on the port so future ReadFile's won't block forever?
|
|
|
|
// Start timer
|
|
|
|
gtimm.AddTimer(this, 10); // Every 100ms, i.e., 1/10 second
|
|
|
|
m_wf |= kfBstSearchingForGames;
|
|
return true;
|
|
}
|
|
|
|
void BtSerTransport::EndGameSearch()
|
|
{
|
|
if ((m_wf & kfBstSearchingForGames) == 0)
|
|
return;
|
|
|
|
// Stop timer
|
|
|
|
gtimm.RemoveTimer(this);
|
|
|
|
CloseOutboundPort();
|
|
m_wf &= ~kfBstSearchingForGames;
|
|
}
|
|
|
|
bool BtSerTransport::AdvertiseGame(const char *pszGameName)
|
|
{
|
|
// Remember the game name we'll be broadcasting
|
|
|
|
strncpyz(m_szGameName, (char *)pszGameName, sizeof(m_szGameName));
|
|
|
|
// Create a socket to broadcast through
|
|
|
|
if (OpenInboundPort() == INVALID_HANDLE_VALUE)
|
|
return false;
|
|
|
|
// Start timer
|
|
|
|
gtimm.AddTimer(this, 50); // Every 500ms, i.e., 1/2 second
|
|
|
|
// Broadcast game availability immediately
|
|
|
|
OnTimer(0);
|
|
m_wf |= kfBstAdvertisingGame;
|
|
return true;
|
|
}
|
|
|
|
void BtSerTransport::UnadvertiseGame(bool fRetainConnections)
|
|
{
|
|
if ((m_wf & kfBstAdvertisingGame) == 0)
|
|
return;
|
|
|
|
// Stop timer
|
|
|
|
gtimm.RemoveTimer(this);
|
|
|
|
// Close broadcast port
|
|
|
|
CloseInboundPort();
|
|
m_wf &= ~kfBstAdvertisingGame;
|
|
}
|
|
|
|
void BtSerTransport::OnTimer(long tCurrent)
|
|
{
|
|
// If we're Advertising send a SERVERINFO message
|
|
|
|
if ((m_wf & kfBstAdvertisingGame) != 0) {
|
|
Assert(m_hfInbound != NULL);
|
|
|
|
// Broadcast a notification that this device is hosting a game
|
|
|
|
ServerInfoNetMessage sinm(m_szGameName);
|
|
MpTrace("> %s", PszFromNetMessage(&sinm));
|
|
|
|
dword cb;
|
|
if (!WriteFile(m_hfInbound, &sinm, sizeof(sinm), &cb, NULL)) {
|
|
#ifdef DEBUG
|
|
HostMessageBox(TEXT("WriteFile err: %d"), GetLastError());
|
|
#endif
|
|
}
|
|
Assert(cb == sizeof(sinm));
|
|
}
|
|
|
|
// If we're searching for hosts check for receipt of a SERVERINFO message
|
|
|
|
if ((m_wf & kfBstSearchingForGames) != 0) {
|
|
Assert(m_hfOutbound != NULL);
|
|
|
|
// Anything coming in on the serial port?
|
|
|
|
COMSTAT coms;
|
|
DWORD dwErrors;
|
|
if (!ClearCommError(m_hfOutbound, &dwErrors, &coms)) {
|
|
#ifdef DEBUG
|
|
HostMessageBox(TEXT("ClearCommError returned %ld"), dwErrors);
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
if (coms.cbInQue >= sizeof(ServerInfoNetMessage)) {
|
|
ServerInfoNetMessage sinm;
|
|
dword cb;
|
|
if (!ReadFile(m_hfOutbound, &sinm, sizeof(ServerInfoNetMessage), &cb, NULL)) {
|
|
#ifdef DEBUG
|
|
HostMessageBox(TEXT("Failed ReadFile ServerInfoNetMessage (%d)"), GetLastError());
|
|
#endif
|
|
return;
|
|
}
|
|
MpTrace("< GAMEHOSTFOUND");
|
|
|
|
if (m_ptcb != NULL) {
|
|
NetAddress nad;
|
|
memset(&nad, 0, sizeof(nad));
|
|
m_ptcb->OnGameHostFound(&nad);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
// BtSerConnection implementation
|
|
|
|
BtSerConnection::BtSerConnection()
|
|
{
|
|
m_hf = NULL;
|
|
m_fListening = false;
|
|
m_fInbound = false;
|
|
gptra->AddConnection(this);
|
|
}
|
|
|
|
BtSerConnection::BtSerConnection(HANDLE hf, bool fInbound)
|
|
{
|
|
m_hf = hf;
|
|
m_fListening = false;
|
|
m_fInbound = fInbound;
|
|
gptra->AddConnection(this);
|
|
}
|
|
|
|
BtSerConnection::~BtSerConnection()
|
|
{
|
|
Disconnect();
|
|
gptra->RemoveConnection(this);
|
|
}
|
|
|
|
bool BtSerConnection::Poll()
|
|
{
|
|
if (m_hf == NULL)
|
|
return false;
|
|
|
|
if (m_fListening) {
|
|
|
|
// Accept a client connection and produce a new file handle for communicating with that client
|
|
|
|
HANDLE hfInbound = ((BtSerTransport *)gptra)->OpenInboundPort();
|
|
if (hfInbound != INVALID_HANDLE_VALUE) {
|
|
Connection *pcon = new BtSerConnection(hfInbound, true);
|
|
if (pcon == NULL)
|
|
return false;
|
|
|
|
if (m_pccb != NULL)
|
|
m_pccb->OnConnect(pcon);
|
|
}
|
|
}
|
|
|
|
{
|
|
// Test if any incoming data is pending
|
|
|
|
COMSTAT coms;
|
|
DWORD dwErrors;
|
|
if (!ClearCommError(m_hf, &dwErrors, &coms)) {
|
|
#ifdef DEBUG
|
|
HostMessageBox(TEXT("ClearCommError returned %ld"), dwErrors);
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
if (coms.cbInQue >= sizeof(NetMessage)) {
|
|
NetMessage nm;
|
|
dword cb;
|
|
if (!ReadFile(m_hf, &nm, sizeof(NetMessage), &cb, NULL)) {
|
|
#ifdef DEBUG
|
|
HostMessageBox(TEXT("Failed ReadFile NetMessage header (%d)"), GetLastError());
|
|
#endif
|
|
}
|
|
if (cb != sizeof(NetMessage)) {
|
|
HandleRecvError();
|
|
return false;
|
|
}
|
|
|
|
// UNDONE: issue for polymorphic NetMessages?
|
|
int cbT = BigWord(nm.cb);
|
|
NetMessage *pnm = (NetMessage *)new byte[cbT];
|
|
if (pnm == NULL) {
|
|
// Data is still pending but the assumption here is that the
|
|
// caller is going to give up (out of memory) and close the socket
|
|
// UNDONE: is this a good assumption?
|
|
return false;
|
|
}
|
|
memcpy(pnm, &nm, sizeof(NetMessage));
|
|
dword cbRemaining = cbT - sizeof(NetMessage);
|
|
if (cbRemaining != 0) {
|
|
dword cbActual;
|
|
if (!ReadFile(m_hf, (pnm + 1), cbRemaining, &cbActual, NULL)) {
|
|
#ifdef DEBUG
|
|
HostMessageBox(TEXT("Failed ReadFile full NetMessage (%d)"), GetLastError());
|
|
#endif
|
|
}
|
|
|
|
if (cbActual != cbRemaining) {
|
|
delete pnm;
|
|
HandleRecvError();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (m_pccb != NULL) {
|
|
// Before calling OnReceive, order in native byte order
|
|
|
|
NetMessageByteOrderSwap(BigWord(pnm->nmid), pnm, false);
|
|
|
|
MpTrace("< %s", PszFromNetMessage(pnm));
|
|
|
|
m_pccb->OnReceive(this, pnm);
|
|
}
|
|
delete pnm;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void BtSerConnection::HandleRecvError()
|
|
{
|
|
Disconnect();
|
|
}
|
|
|
|
void BtSerConnection::HandleSendError()
|
|
{
|
|
Disconnect();
|
|
}
|
|
|
|
bool BtSerConnection::AsyncListen()
|
|
{
|
|
m_hf = ((BtSerTransport *)gptra)->OpenInboundPort();
|
|
if (m_hf == INVALID_HANDLE_VALUE) {
|
|
HtMessageBox(kfMbWhiteBorder, "Communication Error", "Failed to open inbound port.");
|
|
return false;
|
|
}
|
|
|
|
m_fListening = true;
|
|
return true;
|
|
}
|
|
|
|
// UNDONE: this implementation is synchronous
|
|
|
|
bool BtSerConnection::AsyncConnect(NetAddress *pnad)
|
|
{
|
|
m_hf = ((BtSerTransport *)gptra)->OpenOutboundPort();
|
|
if (m_hf == INVALID_HANDLE_VALUE) {
|
|
HtMessageBox(kfMbWhiteBorder, "Communication Error", "Failed to connect.");
|
|
return false;
|
|
}
|
|
|
|
// UNDONE: OnConnectComplete
|
|
|
|
return true;
|
|
}
|
|
|
|
bool BtSerConnection::AsyncSend(NetMessage *pnm)
|
|
{
|
|
if (m_hf == NULL)
|
|
return false;
|
|
|
|
MpTrace("> %s", PszFromNetMessage(pnm));
|
|
|
|
// Before sending, order in network byte order
|
|
|
|
int cb = pnm->cb; // nab this before byte-swapping it!
|
|
NetMessageByteOrderSwap(pnm->nmid, pnm, true);
|
|
|
|
// UNDONE: this is synchronous
|
|
|
|
dword cbActual;
|
|
if (!WriteFile(m_hf, pnm, cb, &cbActual, NULL)) {
|
|
#ifdef DEBUG
|
|
HostMessageBox(TEXT("WriteFile err: %d"), GetLastError());
|
|
#endif
|
|
}
|
|
if (cbActual != cb) {
|
|
HandleSendError();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void BtSerConnection::Disconnect()
|
|
{
|
|
if (m_hf == NULL)
|
|
return;
|
|
|
|
if (m_fInbound)
|
|
((BtSerTransport *)gptra)->CloseInboundPort();
|
|
else
|
|
((BtSerTransport *)gptra)->CloseOutboundPort();
|
|
m_hf = NULL;
|
|
|
|
if (m_pccb != NULL)
|
|
m_pccb->OnDisconnect(this);
|
|
}
|