mirror of https://github.com/lianthony/NT4.0
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1325 lines
38 KiB
1325 lines
38 KiB
//****************************************************************************
|
|
//
|
|
// Module: Unimdm
|
|
// File: mdmasyn.c
|
|
//
|
|
// Copyright (c) 1992-1995, Microsoft Corporation, all rights reserved
|
|
//
|
|
// Revision History
|
|
//
|
|
//
|
|
// 5/4/95 Viroon Touranachun Moved from modem.c
|
|
//
|
|
//
|
|
// Description: Asynchronous thread entry and state machine
|
|
//
|
|
//****************************************************************************
|
|
|
|
#include "unimdm.h"
|
|
#include "umdmspi.h"
|
|
|
|
typedef struct tagMdmThrd {
|
|
struct tagMdmThrd* pNext;
|
|
HANDLE hThrd;
|
|
DWORD tid;
|
|
} MDMTHRD, *PMDMTHRD;
|
|
|
|
// Global asynchronous elements
|
|
//
|
|
PMDMTHRD gpMdmThrdList = NULL;
|
|
HANDLE ghCompletionPort = NULL;
|
|
|
|
extern MDMLIST gMdmList;
|
|
|
|
void MdmCompleteAsync (PLINEDEV pLineDev, DWORD dwStatus, DWORD dwAsyncID);
|
|
void HandleMdmError (PLINEDEV pLineDev, DWORD dwStatus);
|
|
DWORD DetectDialtone (PLINEDEV pLineDev);
|
|
|
|
VOID WINAPI
|
|
ProcessRings(
|
|
PLINEDEV pLineDev,
|
|
DWORD Type
|
|
);
|
|
|
|
|
|
//****************************************************************************
|
|
// DWORD InitializeMdmThreads()
|
|
//
|
|
// Function: Initialize threads to handle modem's asynchronous operations.
|
|
//
|
|
// History:
|
|
// Mon 17-Apr-1995 11:49:53 -by- Viroon Touranachun [viroont]
|
|
// Ported from Win95.
|
|
//****************************************************************************/
|
|
|
|
DWORD InitializeMdmThreads()
|
|
{
|
|
PMDMTHRD pMdmThrd;
|
|
DWORD dwRet = ERROR_SUCCESS; // assume success
|
|
SYSTEM_INFO systeminfo;
|
|
DWORD dwNumThreadsRunning = 0;
|
|
|
|
// We are going to create a thread per processor, so get the system info
|
|
// which contains the number of processors in the system.
|
|
//
|
|
GetSystemInfo(&systeminfo);
|
|
|
|
// Create the completion port. This starts without any file handles being
|
|
// associated with it.
|
|
//
|
|
ASSERT(ghCompletionPort == NULL);
|
|
ghCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE,
|
|
NULL,
|
|
0,
|
|
0);
|
|
if (ghCompletionPort == NULL)
|
|
{
|
|
dwRet = GetLastError();
|
|
DPRINTF1("CreateIoCompletionPort failed with %d", dwRet);
|
|
}
|
|
else
|
|
{
|
|
// Create the threads.
|
|
//
|
|
while (dwNumThreadsRunning < systeminfo.dwNumberOfProcessors)
|
|
{
|
|
if ((pMdmThrd = (PMDMTHRD)LocalAlloc(LPTR, sizeof(*pMdmThrd))) != NULL)
|
|
{
|
|
// Create thread
|
|
//
|
|
pMdmThrd->hThrd = CreateThread(NULL, // default security
|
|
0, // default stack size
|
|
MdmAsyncThread, // thread entry point
|
|
pMdmThrd, // thread info
|
|
CREATE_SUSPENDED, // Start suspended
|
|
&pMdmThrd->tid); // thread id
|
|
|
|
if (pMdmThrd->hThrd != NULL)
|
|
{
|
|
dwNumThreadsRunning++;
|
|
|
|
// Put it in the thread list
|
|
//
|
|
pMdmThrd->pNext = gpMdmThrdList;
|
|
gpMdmThrdList = pMdmThrd;
|
|
|
|
DPRINTF1("Async Thread id: %x was created but suspended", pMdmThrd->tid);
|
|
|
|
// Send thread on its way...
|
|
//
|
|
ResumeThread(pMdmThrd->hThrd);
|
|
|
|
DPRINTF1("Async Thread id: %x is in operation", pMdmThrd->tid);
|
|
}
|
|
else
|
|
{
|
|
LocalFree(pMdmThrd);
|
|
|
|
// If we were able to get at least one thread going, indicate success.
|
|
//
|
|
if (dwNumThreadsRunning == 0)
|
|
{
|
|
DPRINTF("InitializeMdmThreads was unable to create all of the threads! (CreateThread failed)");
|
|
dwRet = LINEERR_OPERATIONFAILED;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If we were able to get at least one thread going, indicate success.
|
|
//
|
|
if (dwNumThreadsRunning == 0)
|
|
{
|
|
DPRINTF("InitializeMdmThreads was unable to create all of the threads! (LocalAlloc failed)");
|
|
dwRet = ERROR_OUTOFMEMORY;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we failed in some way, delete the completion port, it isn't being used.
|
|
//
|
|
if (dwRet != ERROR_SUCCESS)
|
|
{
|
|
ASSERT(dwNumThreadsRunning == 0);
|
|
|
|
if (ghCompletionPort != NULL)
|
|
{
|
|
CloseHandle(ghCompletionPort);
|
|
ghCompletionPort = NULL;
|
|
}
|
|
}
|
|
|
|
return dwRet;
|
|
}
|
|
|
|
//****************************************************************************
|
|
// DWORD DeinitializeMdmThreads()
|
|
//
|
|
// Function: Deinitialize threads to handle modem's asynchronous operations.
|
|
//
|
|
// History:
|
|
// Mon 17-Apr-1995 11:49:53 -by- Viroon Touranachun [viroont]
|
|
// Ported from Win95.
|
|
//****************************************************************************/
|
|
|
|
DWORD DeinitializeMdmThreads()
|
|
{
|
|
PMDMTHRD pMdmThrd;
|
|
DWORD dwCount = 0;
|
|
HANDLE WaitHandles[MAXIMUM_WAIT_OBJECTS];
|
|
|
|
DPRINTF("DeinitializeMdmThreads() called.");
|
|
|
|
// Post a termination request to the completion port.
|
|
// Do one for each thread running. Keep count for later in this routine.
|
|
//
|
|
pMdmThrd = gpMdmThrdList;
|
|
while (pMdmThrd)
|
|
{
|
|
PostQueuedCompletionStatus(ghCompletionPort,
|
|
CP_BYTES_WRITTEN(0), // indicates nothing
|
|
0, // indicates this is a termination message
|
|
NULL); // indicates nothing
|
|
dwCount++;
|
|
|
|
pMdmThrd = pMdmThrd->pNext;
|
|
}
|
|
|
|
ASSERT(dwCount <= MAXIMUM_WAIT_OBJECTS); // Make sure the world hasn't turned upside down.
|
|
|
|
// Do a WaitForMultipleObjects on all of the thread handles to wait for
|
|
// them to complete.
|
|
//
|
|
// Build array to wait on.
|
|
//
|
|
dwCount = 0;
|
|
pMdmThrd = gpMdmThrdList;
|
|
while (pMdmThrd)
|
|
{
|
|
WaitHandles[dwCount++] = pMdmThrd->hThrd;
|
|
|
|
pMdmThrd = pMdmThrd->pNext;
|
|
}
|
|
|
|
WaitForMultipleObjects(dwCount, // number of threads to wait to end
|
|
WaitHandles, // array of threads
|
|
TRUE, // wait until all have been terminated
|
|
INFINITE); // wait forever, if necessary
|
|
|
|
while (pMdmThrd = gpMdmThrdList)
|
|
{
|
|
CloseHandle(pMdmThrd->hThrd);
|
|
|
|
gpMdmThrdList = pMdmThrd->pNext;
|
|
LocalFree(pMdmThrd);
|
|
}
|
|
|
|
//
|
|
// close completion port handle
|
|
//
|
|
CloseHandle(ghCompletionPort);
|
|
|
|
DPRINTF("Async Threads terminated succesfully.");
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
//****************************************************************************
|
|
// DWORD APIENTRY MdmAsyncThread (PMDMTHRD)
|
|
//
|
|
// Function: An entry point to the asynchronous modem thread.
|
|
//
|
|
// Returns: None
|
|
//
|
|
//****************************************************************************/
|
|
|
|
DWORD APIENTRY MdmAsyncThread (PMDMTHRD pMdmThrd)
|
|
{
|
|
DWORD dwNumberOfBytesTransferred;
|
|
DWORD dwCompletionKey;
|
|
LPOVERLAPPED lpOverlapped;
|
|
PLINEDEV pLineDev;
|
|
|
|
DPRINTF1("Async thread id: %x is running", pMdmThrd->tid);
|
|
|
|
ASSERT(ghCompletionPort != NULL);
|
|
|
|
// Read from the completion port until we get signalled to exit the thread.
|
|
// This is done by having a completion posted that has a dwCompletionKey of 0.
|
|
// dwCompletionKey is normally the pLineDev associated with an operation.
|
|
//
|
|
for (;;)
|
|
{
|
|
if (GetQueuedCompletionStatus(ghCompletionPort,
|
|
&dwNumberOfBytesTransferred,
|
|
&dwCompletionKey,
|
|
&lpOverlapped,
|
|
(DWORD)-1) == FALSE &&
|
|
lpOverlapped == NULL)
|
|
{
|
|
DPRINTF1("GetQueuedCompletionStatus() returned FALSE and lpOverlapped was NULL (GetLastError() = %d)",
|
|
GetLastError());
|
|
ASSERT(0);
|
|
continue;
|
|
}
|
|
|
|
TRACE4(
|
|
IDEVENT_CP_GET,
|
|
dwNumberOfBytesTransferred,
|
|
dwCompletionKey,
|
|
lpOverlapped
|
|
);
|
|
|
|
if (dwCompletionKey == 0)
|
|
{
|
|
DPRINTF1("Async Thread id: %d being asked to exit", pMdmThrd->tid);
|
|
break; // NULL dwCompletionKey indicates we were asked to terminate.
|
|
}
|
|
else
|
|
{
|
|
pLineDev = (PLINEDEV)dwCompletionKey;
|
|
CLAIM_LINEDEV(pLineDev);
|
|
|
|
// BUGBUG: CCaputo 2/20/96: pLineDev could have disappeard.
|
|
// BUGBUG: Might want to do something here to verify existence.
|
|
|
|
// Is this completion for the upper layer state machine (unimdm) or the
|
|
// lower layer state machine (mcx)?
|
|
//
|
|
if (lpOverlapped == NULL)
|
|
{
|
|
DWORD dwType = CP_TYPE(dwNumberOfBytesTransferred);
|
|
if (dwType != 0) {
|
|
//
|
|
// it is a ring related event
|
|
//
|
|
ProcessRings(
|
|
pLineDev,
|
|
dwType
|
|
);
|
|
|
|
} else {
|
|
//
|
|
// Is the purpose of this completion to signal an event or
|
|
// continue in the normal unimdm state machine?
|
|
//
|
|
if (pLineDev->hSynchronizeEvent == NULL)
|
|
{
|
|
DWORD dwPendingID;
|
|
|
|
dwPendingID = pLineDev->McxOut.dwReqID;
|
|
pLineDev->McxOut.dwReqID = MDM_ID_NULL;
|
|
|
|
// Call the operation handler
|
|
//
|
|
MdmCompleteAsync(pLineDev, pLineDev->McxOut.dwResult, dwPendingID);
|
|
}
|
|
else
|
|
{
|
|
SetEvent(pLineDev->hSynchronizeEvent);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// non-NULL lpOverlapped indicates this is for MCXAsyncComplete()
|
|
//
|
|
if (pLineDev->hModem)
|
|
{
|
|
MCXAsyncComplete (pLineDev->hModem, lpOverlapped);
|
|
}
|
|
|
|
OverPoolFree(lpOverlapped);
|
|
}
|
|
|
|
RELEASE_LINEDEV(pLineDev);
|
|
}
|
|
}
|
|
|
|
// Exit the thread properly
|
|
//
|
|
DPRINTF1("Async Thread id: %d exits", pMdmThrd->tid);
|
|
ExitThread(ERROR_SUCCESS);
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
|
|
//
|
|
// Rings are handle here instead of the main async handler beacuse they get built
|
|
// up in the completion port and extra one mess up the state machine
|
|
//
|
|
VOID WINAPI
|
|
ProcessRings(
|
|
PLINEDEV pLineDev,
|
|
DWORD Type
|
|
)
|
|
|
|
{
|
|
|
|
|
|
switch (pLineDev->DevState) {
|
|
|
|
case DEVST_PORTLISTENING:
|
|
//
|
|
// The line is being monitored and the first ring is coming in.
|
|
//
|
|
|
|
// Make sure call hasn't already been allocated.
|
|
// If this isn't the first set of rings, then make
|
|
// sure the previous "hdcall" has been deallocated.
|
|
// In other words, ignore rings until this happens!
|
|
//
|
|
if (pLineDev->dwCall & CALL_ALLOCATED)
|
|
{
|
|
TSPPRINTF("RING ignored because IDLE call hasn't been deallocated!");
|
|
break;
|
|
};
|
|
|
|
// We need to notify a new call to TAPI
|
|
//
|
|
(*(pLineDev->lpfnEvent))(pLineDev->htLine, NULL, LINE_NEWCALL,
|
|
(DWORD)pLineDev,
|
|
(DWORD)((LPHANDLE)&(pLineDev->htCall)),
|
|
0);
|
|
|
|
// Allocate the call
|
|
//
|
|
pLineDev->dwCall = CALL_ALLOCATED | CALL_INBOUND | CALL_ACTIVE;
|
|
|
|
// Then offer the call to TAPI
|
|
//
|
|
pLineDev->DevState = DEVST_PORTLISTENOFFER;
|
|
|
|
// OR in UNKNOWN since we don't know what kind of media mode this call is
|
|
//
|
|
pLineDev->dwCurMediaModes = pLineDev->dwDetMediaModes | LINEMEDIAMODE_UNKNOWN;
|
|
|
|
// default our bearermode to be what we support, excluding the passthrough bit
|
|
//
|
|
pLineDev->dwCurBearerModes = pLineDev->dwBearerModes & ~LINEBEARERMODE_PASSTHROUGH;
|
|
|
|
// Notify TAPI
|
|
//
|
|
NEW_CALLSTATE(pLineDev, LINECALLSTATE_OFFERING, 0);
|
|
|
|
//
|
|
// fall through
|
|
//
|
|
|
|
|
|
case DEVST_PORTLISTENOFFER:
|
|
{
|
|
//
|
|
// A ring is coming in (either the first ring or a subsequent one.)
|
|
// Handle the ring count
|
|
//
|
|
DWORD dwCurrent = GETTICKCOUNT();
|
|
|
|
// If this is the second or greater ring,
|
|
// then we may have a timer to kill.
|
|
//
|
|
if (pLineDev->dwRingCount)
|
|
{
|
|
KillMdmTimer((DWORD)pLineDev, NULL);
|
|
|
|
// Check whether the timeout expired
|
|
//
|
|
if (GTC_DELTA(pLineDev->dwRingTick, dwCurrent) >= TO_MS_RING_SEPARATION)
|
|
{
|
|
// Timeout has expired, indicating call has stopped ringing
|
|
//
|
|
pLineDev->dwRingTick = 0;
|
|
pLineDev->dwRingCount = 0;
|
|
pLineDev->DevState = DEVST_PORTLISTENING;
|
|
NEW_CALLSTATE(pLineDev, LINECALLSTATE_IDLE, 0);
|
|
break;
|
|
};
|
|
};
|
|
|
|
pLineDev->dwRingCount++;
|
|
(*pLineDev->lpfnEvent)(pLineDev->htLine, NULL, LINE_LINEDEVSTATE,
|
|
LINEDEVSTATE_RINGING, 1L, pLineDev->dwRingCount);
|
|
TSPPRINTF1("RING#%d notfied", pLineDev->dwRingCount);
|
|
|
|
pLineDev->dwRingTick = dwCurrent;
|
|
if (SetMdmTimer((DWORD)pLineDev, NULL, TO_MS_RING_SEPARATION)
|
|
!= ERROR_SUCCESS)
|
|
{
|
|
TSPPRINTF("SetTimer failed!");
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
|
|
TSPPRINTF("ProcessRings: extra ring queued!");
|
|
|
|
break;
|
|
|
|
} // switch
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//****************************************************************************
|
|
// void MdmCompleteAsync(PLINEDEV, DWORD, DWORD)
|
|
//
|
|
// Function: A caller invokes this function when it receives WM_COMNOTIFY
|
|
// message in order to complete the pending asynchronous operation
|
|
// or asynchronous event.
|
|
//
|
|
// Note: The dialing process is pretty complex. See dialing.txt for more info.
|
|
//
|
|
// Returns: nothing
|
|
//
|
|
//****************************************************************************
|
|
|
|
void MdmCompleteAsync (PLINEDEV pLineDev, DWORD dwStatus, DWORD dwAsyncID)
|
|
{
|
|
DWORD dwRet;
|
|
|
|
ASSERT(pLineDev->pDevCfg != NULL);
|
|
TSPPRINTF2("MdmCompleteAsync services id: %d status: %d", dwAsyncID, dwStatus);
|
|
|
|
// We only care about messages we are expecting or
|
|
// MDM_ID_NULL (unexpected) messages
|
|
//
|
|
if (dwAsyncID != MDM_ID_NULL)
|
|
{
|
|
if (pLineDev->dwVxdPendingID == dwAsyncID)
|
|
{
|
|
// Reset the pending ID, except for when we are doing
|
|
// continuous monitoring from the VxD.
|
|
//
|
|
if (DEVST_PORTLISTENING != pLineDev->DevState &&
|
|
DEVST_PORTLISTENOFFER != pLineDev->DevState)
|
|
{
|
|
pLineDev->dwVxdPendingID = MDM_ID_NULL;
|
|
};
|
|
}
|
|
else
|
|
{
|
|
TSPPRINTF1("rejecting obsolete async id: %d", dwAsyncID);
|
|
return;
|
|
};
|
|
};
|
|
|
|
// Messages from the VxD when we are in takeover mode are used to do the
|
|
// async completion for the switch to takeover mode.
|
|
//
|
|
if (pLineDev->fTakeoverMode)
|
|
{
|
|
if (pLineDev->dwPendingID != INVALID_PENDINGID)
|
|
{
|
|
UnimodemSetPassthrough(pLineDev, PASSTHROUGH_ON);
|
|
pLineDev->DevState = DEVST_CONNECTED;
|
|
(*gfnCompletionCallback)(pLineDev->dwPendingID, ERROR_SUCCESS);
|
|
pLineDev->dwPendingID = INVALID_PENDINGID;
|
|
NEW_CALLSTATE(pLineDev, LINECALLSTATE_CONNECTED, 0);
|
|
};
|
|
return;
|
|
};
|
|
|
|
// If it is the success notification, we continue the state machine
|
|
//
|
|
if (MDM_SUCCESS == dwStatus)
|
|
{
|
|
do
|
|
{
|
|
// Yes, it is. Determine the current line state
|
|
//
|
|
dwRet = ERROR_IO_PENDING;
|
|
|
|
switch (pLineDev->DevState)
|
|
{
|
|
case DEVST_PORTLISTENINIT:
|
|
//
|
|
// Put modem to non-continuous Monitor
|
|
//
|
|
pLineDev->DevState = DEVST_PORTLISTENING;
|
|
pLineDev->dwRingCount = 0;
|
|
pLineDev->dwRingTick = 0;
|
|
|
|
// Start monitoring the line
|
|
//
|
|
dwRet = UnimodemMonitor(pLineDev, MONITOR_CONTINUOUS);
|
|
break;
|
|
|
|
case DEVST_PORTSTARTPRETERMINAL:
|
|
//
|
|
// Start the terminal screen
|
|
//
|
|
pLineDev->DevState = DEVST_PORTCONNECTINIT;
|
|
dwRet = ERROR_SUCCESS;
|
|
|
|
// Turn-on passthrough mode
|
|
//
|
|
if (UnimodemSetPassthrough(pLineDev, PASSTHROUGH_ON) == ERROR_SUCCESS)
|
|
{
|
|
// Put the terminal screen up here
|
|
//
|
|
pLineDev->DevState = DEVST_PORTPRETERMINAL;
|
|
|
|
if (TerminalDialog(pLineDev) == ERROR_SUCCESS)
|
|
{
|
|
// Wait until the terminal screen completes
|
|
//
|
|
dwRet = ERROR_IO_PENDING;
|
|
};
|
|
};
|
|
break;
|
|
|
|
case DEVST_PORTPRETERMINAL:
|
|
//
|
|
// Destroy the terminal window
|
|
//
|
|
DestroyTerminalDialog(pLineDev);
|
|
|
|
// Turn-off passthrough mode
|
|
//
|
|
UnimodemSetPassthrough(pLineDev, PASSTHROUGH_OFF);
|
|
|
|
// Put it to init mode
|
|
//
|
|
pLineDev->DevState = DEVST_PORTCONNECTINIT;
|
|
dwRet = UnimodemInit(pLineDev);
|
|
break;
|
|
|
|
case DEVST_PORTCONNECTINIT:
|
|
//
|
|
// The modem was sucessfully initialized for dialing out.
|
|
//
|
|
if (!pLineDev->InitStringsAreValid) {
|
|
//
|
|
// some one call lineSetDevConfig, redo the init
|
|
//
|
|
pLineDev->DevState = DEVST_PORTCONNECTINIT;
|
|
dwRet = UnimodemInit(pLineDev);
|
|
|
|
break;
|
|
}
|
|
|
|
|
|
pLineDev->DevState = DEVST_PORTCONNECTDIALTONEDETECT;
|
|
pLineDev->dwCall |= CALL_ACTIVE;
|
|
|
|
// Detect dialtone
|
|
//
|
|
dwRet = DetectDialtone(pLineDev);
|
|
break;
|
|
|
|
case DEVST_PORTCONNECTWAITFORLINEDIAL:
|
|
case DEVST_PORTCONNECTDIALTONEDETECT:
|
|
//
|
|
// The dialtone was detected or we did not need to detect it. Now we
|
|
// are ready to dial.
|
|
//
|
|
// Note: The dialing process is pretty complex.
|
|
// (See dialing.txt for more info.)
|
|
//
|
|
pLineDev->dwCall |= CALL_ACTIVE;
|
|
|
|
// Check for needed async completion.
|
|
// (for the lineMakeCall)
|
|
//
|
|
if (pLineDev->dwPendingID != INVALID_PENDINGID &&
|
|
pLineDev->dwPendingType == PENDING_LINEMAKECALL)
|
|
{
|
|
(*gfnCompletionCallback)(pLineDev->dwPendingID, 0L);
|
|
pLineDev->dwPendingID = INVALID_PENDINGID;
|
|
pLineDev->dwPendingType = INVALID_PENDINGOP;
|
|
NEW_CALLSTATE(pLineDev, LINECALLSTATE_DIALTONE, LINEDIALTONEMODE_UNAVAIL);
|
|
};
|
|
|
|
// Fall through to common code path
|
|
//
|
|
case DEVST_MANUALDIALING:
|
|
//
|
|
// If it is an originate address (no semi-colone at the end,) this is
|
|
// the last string we are dialing before connecting.
|
|
//
|
|
if (IsOriginateAddress(pLineDev->szAddress))
|
|
{
|
|
// If we re-enter after manual dialing,
|
|
// do not repeat manual dialing
|
|
//
|
|
if (pLineDev->DevState != DEVST_MANUALDIALING)
|
|
{
|
|
// Don't optimize this and the one below!!! (see lineGetCallStatus)
|
|
//
|
|
pLineDev->DevState = DEVST_PORTCONNECTING;
|
|
NEW_CALLSTATE(pLineDev, LINECALLSTATE_DIALING, 0);
|
|
|
|
// Handle Manual Dial
|
|
//
|
|
if (GETOPTIONS(pLineDev->pDevCfg) & MANUAL_DIAL)
|
|
{
|
|
|
|
// bring up modal manual dial dialog,
|
|
// it will return ERROR_SUCCESS or a non-pending error code
|
|
// when it is done...
|
|
//
|
|
dwRet = ManualDialog(pLineDev);
|
|
|
|
// If there is no error, wait for dialog to finish
|
|
//
|
|
if (ERROR_SUCCESS == dwRet)
|
|
{
|
|
dwRet = ERROR_IO_PENDING;
|
|
pLineDev->DevState = DEVST_MANUALDIALING;
|
|
};
|
|
break;
|
|
};
|
|
}
|
|
else
|
|
{
|
|
*pLineDev->szAddress = '\0';
|
|
|
|
// We finish manual dialing, continue
|
|
//
|
|
pLineDev->DevState = DEVST_PORTCONNECTING;
|
|
};
|
|
|
|
NEW_CALLSTATE(pLineDev, LINECALLSTATE_PROCEEDING, 0);
|
|
|
|
// Handle INTERACTIVEVOICE
|
|
//
|
|
if (LINEMEDIAMODE_INTERACTIVEVOICE == pLineDev->dwCurMediaModes)
|
|
{
|
|
// if we have partial dialing capability and enough room to do it,
|
|
// then make it so we wait indefinitely.
|
|
//
|
|
if (pLineDev->fPartialDialing &&
|
|
lstrlenA(pLineDev->szAddress) + lstrlenA(szSemicolon)
|
|
< sizeof(pLineDev->szAddress))
|
|
{
|
|
lstrcatA(pLineDev->szAddress, szSemicolon);
|
|
};
|
|
|
|
// bring up talk drop dialog,
|
|
//
|
|
if ((dwRet = TalkDropDialog(pLineDev)) == ERROR_SUCCESS)
|
|
{
|
|
// Don't dial if we are MANUAL dialing.
|
|
//
|
|
if (!(GETOPTIONS(pLineDev->pDevCfg) & MANUAL_DIAL))
|
|
{
|
|
// Dial the number and either:
|
|
// 1) wait indefinitely, if we support partial dialing, or
|
|
// 2) begin busy monitoring for several seconds
|
|
// (register S7, dwCallSetupFailTimer)
|
|
//
|
|
if ((dwRet = UnimodemDial(pLineDev, pLineDev->szAddress, pLineDev->dwDialOptions))
|
|
== ERROR_IO_PENDING)
|
|
{
|
|
pLineDev->DevState = DEVST_TALKDROPDIALING;
|
|
};
|
|
}
|
|
else
|
|
{
|
|
// Wait until the user takes an action
|
|
//
|
|
dwRet = ERROR_IO_PENDING;
|
|
};
|
|
};
|
|
};
|
|
}
|
|
else
|
|
{
|
|
// Don't optimize this and the one above!!! (see lineGetCallStatus)
|
|
//
|
|
pLineDev->DevState = DEVST_PORTCONNECTDIAL;
|
|
NEW_CALLSTATE(pLineDev, LINECALLSTATE_DIALING, 0);
|
|
};
|
|
|
|
// INTERACTIVEVOICE Originate's are handled above!
|
|
//
|
|
if (LINEMEDIAMODE_INTERACTIVEVOICE != pLineDev->dwCurMediaModes ||
|
|
DEVST_PORTCONNECTDIAL == pLineDev->DevState)
|
|
{
|
|
// Do we have anything useful to dial? (more than just ";"?)
|
|
//
|
|
if (lstrcmpA(pLineDev->szAddress, szSemicolon))
|
|
{
|
|
// Start the dialing process
|
|
//
|
|
dwRet = UnimodemDial(pLineDev, pLineDev->szAddress, pLineDev->dwDialOptions);
|
|
}
|
|
else
|
|
{
|
|
// just skip to the next stage
|
|
//
|
|
dwRet = ERROR_SUCCESS;
|
|
};
|
|
}
|
|
|
|
// Check for needed async completion.
|
|
// (for the lineDial)
|
|
//
|
|
if (pLineDev->dwPendingID != INVALID_PENDINGID &&
|
|
pLineDev->dwPendingType == PENDING_LINEDIAL)
|
|
{
|
|
(*gfnCompletionCallback)(pLineDev->dwPendingID, 0L);
|
|
pLineDev->dwPendingID = INVALID_PENDINGID;
|
|
pLineDev->dwPendingType = INVALID_PENDINGOP;
|
|
};
|
|
|
|
break;
|
|
|
|
case DEVST_PORTCONNECTDIAL:
|
|
//
|
|
// The line was previously dialed with a non-originate address
|
|
// Wait for another lineDial
|
|
//
|
|
pLineDev->DevState = DEVST_PORTCONNECTWAITFORLINEDIAL;
|
|
pLineDev->dwCall |= CALL_ACTIVE;
|
|
|
|
// Send up another LINECALLSTATE_DIALING so that app knows
|
|
// to check the call status to see that lineDial is usable again.
|
|
//
|
|
NEW_CALLSTATE(pLineDev, LINECALLSTATE_DIALING, 0);
|
|
break;
|
|
|
|
case DEVST_TALKDROPDIALING:
|
|
//
|
|
// Result from UnimodemDial
|
|
//
|
|
pLineDev->DevState = DEVST_PORTCONNECTING;
|
|
|
|
// Only pay attention to these messages if we are actually originating
|
|
// (monitoring) or if it is an error from the partial dial.
|
|
// (ignore MDM_SUCCESS from partial dial)
|
|
//
|
|
if (IsOriginateAddress(pLineDev->szAddress))
|
|
{
|
|
DestroyTalkDropDialog(pLineDev);
|
|
dwRet = ERROR_SUCCESS;
|
|
}
|
|
break;
|
|
|
|
|
|
case DEVST_PORTCONNECTING:
|
|
//
|
|
// The modem sucessfully dial the originate address.
|
|
// The modem is connected.
|
|
//
|
|
pLineDev->DevState = DEVST_PORTLISTENANSWER;
|
|
dwRet = ERROR_SUCCESS;
|
|
|
|
// Post-dial Terminal Mode
|
|
//
|
|
if (GETOPTIONS(pLineDev->pDevCfg) & TERMINAL_POST)
|
|
{
|
|
pLineDev->DevState = DEVST_PORTPOSTTERMINAL;
|
|
if (TerminalDialog(pLineDev) == ERROR_SUCCESS)
|
|
{
|
|
dwRet = ERROR_IO_PENDING;
|
|
};
|
|
};
|
|
break;
|
|
|
|
case DEVST_PORTPOSTTERMINAL:
|
|
//
|
|
// Destroy the termnal window
|
|
//
|
|
DestroyTerminalDialog(pLineDev);
|
|
pLineDev->DevState = DEVST_PORTLISTENANSWER;
|
|
dwRet = ERROR_SUCCESS;
|
|
break;
|
|
|
|
case DEVST_PORTLISTENANSWER:
|
|
//
|
|
// The modem is connected (with either incoming or outgoing call.)
|
|
// Ready to notify TAPI of the connected line.
|
|
//
|
|
// Treat INTERACTIVEVOICE connections differently.
|
|
//
|
|
if (LINEMEDIAMODE_INTERACTIVEVOICE != pLineDev->dwCurMediaModes)
|
|
{
|
|
// Get the call information
|
|
//
|
|
UnimodemGetNegotiatedRate(pLineDev, (LPDWORD)&pLineDev->dwNegotiatedRate);
|
|
|
|
//
|
|
// Start monitoring the remote disconnection here
|
|
//
|
|
UnimodemMonitorDisconnect(pLineDev);
|
|
|
|
// Do we need to lauch modem light?
|
|
// We launch the light when the light was selected.
|
|
//
|
|
if (!IS_NULL_MODEM(pLineDev) &&
|
|
(GETOPTIONS(pLineDev->pDevCfg) & LAUNCH_LIGHTS))
|
|
{
|
|
HANDLE hLight;
|
|
|
|
if (LaunchModemLight(pLineDev->szDeviceName,
|
|
pLineDev->hDevice,
|
|
&hLight) == ERROR_SUCCESS)
|
|
pLineDev->hLights = hLight;
|
|
};
|
|
};
|
|
|
|
// Notify TAPI of the connected line
|
|
//
|
|
pLineDev->DevState = DEVST_CONNECTED;
|
|
NEW_CALLSTATE(pLineDev, LINECALLSTATE_CONNECTED, 0);
|
|
break;
|
|
|
|
case DEVST_DISCONNECTING:
|
|
|
|
TSPPRINTF("Setting Drop Event");
|
|
|
|
|
|
SetEvent(
|
|
pLineDev->DroppingEvent
|
|
);
|
|
|
|
// case DEVST_DISCONNECTED:
|
|
//
|
|
// The modem was hung up successfully.
|
|
//
|
|
if (pLineDev->dwPendingID != INVALID_PENDINGID)
|
|
{
|
|
// Notify TAPI of the idle line
|
|
//
|
|
NEW_CALLSTATE(pLineDev, LINECALLSTATE_IDLE, 0);
|
|
|
|
(*(gfnCompletionCallback))(pLineDev->dwPendingID, 0L);
|
|
pLineDev->dwPendingID = INVALID_PENDINGID;
|
|
pLineDev->dwPendingType = INVALID_PENDINGOP;
|
|
};
|
|
|
|
pLineDev->DevState = DEVST_DISCONNECTED;
|
|
|
|
break;
|
|
|
|
default:
|
|
pLineDev->DevState = DEVST_DISCONNECTED;
|
|
break;
|
|
};
|
|
|
|
// We may have a new failure
|
|
//
|
|
if ((dwRet != ERROR_IO_PENDING) && (dwRet != ERROR_SUCCESS))
|
|
{
|
|
dwStatus = MDM_FAILURE;
|
|
};
|
|
|
|
} while (dwRet == ERROR_SUCCESS);
|
|
};
|
|
|
|
// Handle failure
|
|
//
|
|
if ((dwStatus != MDM_SUCCESS) && (dwStatus != MDM_PENDING))
|
|
{
|
|
HandleMdmError(pLineDev, dwStatus);
|
|
};
|
|
return;
|
|
}
|
|
|
|
//****************************************************************************
|
|
// DWORD MdmAsyncContinue(PLINEDEV, DWORD)
|
|
//
|
|
// Function: continues the next state for the modem device
|
|
//
|
|
// Returns: ERROR_SUCCESS
|
|
//
|
|
//****************************************************************************
|
|
|
|
DWORD MdmAsyncContinue (PLINEDEV pLineDev, DWORD dwStatus)
|
|
{
|
|
pLineDev->dwVxdPendingID++;
|
|
pLineDev->McxOut.dwReqID = pLineDev->dwVxdPendingID;
|
|
pLineDev->McxOut.dwResult = dwStatus;
|
|
|
|
PostQueuedCompletionStatus(ghCompletionPort,
|
|
CP_BYTES_WRITTEN(0),
|
|
(DWORD)pLineDev,
|
|
NULL);
|
|
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
//****************************************************************************
|
|
// void HandleMdmError(PLINEDEV, DWORD)
|
|
//
|
|
// Function: Handles the modem line when the status is not successful.
|
|
//
|
|
// Returns: nothing
|
|
//
|
|
//****************************************************************************
|
|
|
|
void HandleMdmError(PLINEDEV pLineDev, DWORD dwStatus)
|
|
{
|
|
// Do we have a call?
|
|
//
|
|
if ((pLineDev->dwCall & CALL_ALLOCATED) &&
|
|
(pLineDev->dwCallState != LINECALLSTATE_DISCONNECTED))
|
|
{
|
|
DWORD dwDisconnectMode = 0;
|
|
LONG lAsyncResult = 0;
|
|
|
|
// Terminate all UI windows
|
|
//
|
|
DestroyTalkDropDialog(pLineDev);
|
|
DestroyManualDialog(pLineDev);
|
|
|
|
// Determine the failure
|
|
//
|
|
switch (dwStatus)
|
|
{
|
|
case MDM_HANGUP:
|
|
//
|
|
// The line is disconnected remotely, or the user cancel the line
|
|
// Destroy the terminal window
|
|
//
|
|
|
|
if ((DEVST_PORTPRETERMINAL == pLineDev->DevState)
|
|
||
|
|
(DEVST_PORTPOSTTERMINAL == pLineDev->DevState)) {
|
|
|
|
DestroyTerminalDialog(pLineDev);
|
|
lAsyncResult = LINEERR_OPERATIONFAILED;
|
|
}
|
|
|
|
dwDisconnectMode = LINEDISCONNECTMODE_NORMAL;
|
|
break;
|
|
|
|
case MDM_BUSY:
|
|
//
|
|
// We dialed out and the line is busy
|
|
//
|
|
dwDisconnectMode = LINEDISCONNECTMODE_BUSY;
|
|
break;
|
|
|
|
case MDM_NOANSWER:
|
|
case MDM_NOCARRIER:
|
|
//
|
|
// We dialed out and nobody answered the phone
|
|
//
|
|
dwDisconnectMode = LINEDISCONNECTMODE_NOANSWER;
|
|
break;
|
|
|
|
case MDM_NODIALTONE:
|
|
//
|
|
// We were dialing out but no dial tone on the line
|
|
// were we checking for a dialtone?
|
|
//
|
|
if (DEVST_PORTCONNECTDIALTONEDETECT == pLineDev->DevState &&
|
|
!(pLineDev->dwDialOptions & MDM_BLIND_DIAL))
|
|
{
|
|
lAsyncResult = LINEERR_CALLUNAVAIL;
|
|
};
|
|
dwDisconnectMode = LINEDISCONNECTMODE_NODIALTONE;
|
|
break;
|
|
|
|
case MDM_FAILURE:
|
|
default:
|
|
//
|
|
// The pending operation failed
|
|
//
|
|
if (DEVST_MANUALDIALING == pLineDev->DevState) {
|
|
//
|
|
// cancel was pressed on the manual dial dialog
|
|
//
|
|
dwDisconnectMode = LINEDISCONNECTMODE_CANCELLED;
|
|
|
|
} else {
|
|
|
|
dwDisconnectMode = LINEDISCONNECTMODE_UNAVAIL;
|
|
}
|
|
|
|
// If it is a failed makecall, need to return a failure
|
|
//
|
|
if (pLineDev->dwPendingType == PENDING_LINEMAKECALL)
|
|
{
|
|
|
|
lAsyncResult = LINEERR_OPERATIONFAILED;
|
|
};
|
|
break;
|
|
};
|
|
|
|
// No need for further UI
|
|
//
|
|
DestroyMdmDlgInstance(pLineDev);
|
|
|
|
// In any case, we need to notify TAPI to clean up the line
|
|
//
|
|
NEW_CALLSTATE(pLineDev, LINECALLSTATE_DISCONNECTED, dwDisconnectMode);
|
|
NEW_CALLSTATE(pLineDev, LINECALLSTATE_IDLE, 0);
|
|
pLineDev->DevState = DEVST_DISCONNECTED;
|
|
|
|
// Notify the caller async completion
|
|
//
|
|
if (pLineDev->dwPendingID != INVALID_PENDINGID)
|
|
{
|
|
(*gfnCompletionCallback)(pLineDev->dwPendingID, lAsyncResult);
|
|
pLineDev->dwPendingID = INVALID_PENDINGID;
|
|
|
|
// If it is a failed makecall, we need to close the line
|
|
//
|
|
if (pLineDev->dwPendingType == PENDING_LINEMAKECALL)
|
|
{
|
|
|
|
if ((pLineDev->dwDetMediaModes) && !(pLineDev->fdwResources & LINEDEVFLAGS_OUTOFSERVICE)) {
|
|
|
|
if ((DevlineDetectCall(pLineDev)) != ERROR_SUCCESS) {
|
|
//
|
|
// init failed, tell the app
|
|
//
|
|
pLineDev->LineClosed=TRUE;
|
|
|
|
(*pLineDev->lpfnEvent)(pLineDev->htLine, NULL, LINE_CLOSE,
|
|
0L, 0L, 0L);
|
|
}
|
|
|
|
} else {
|
|
//
|
|
// No need to detect the line, just close it.
|
|
//
|
|
DevlineClose(pLineDev);
|
|
}
|
|
|
|
|
|
#ifdef UNDER_CONSTRUCTION
|
|
if (pLineDev->fdwResources & LINEDEVFLAGS_OUTOFSERVICE)
|
|
{
|
|
DevlineDisabled(pLineDev);
|
|
}
|
|
else
|
|
#endif // UNDER_CONSTRUCTION
|
|
{
|
|
pLineDev->dwPendingType = INVALID_PENDINGOP;
|
|
|
|
// Clean up the call state of this line
|
|
//
|
|
// pLineDev->htCall = NULL; will be cleaned up TSPI_lineClosecall
|
|
pLineDev->dwCall = 0L;
|
|
};
|
|
}
|
|
else
|
|
{
|
|
pLineDev->dwPendingType = INVALID_PENDINGOP;
|
|
};
|
|
};
|
|
}
|
|
else
|
|
{
|
|
// A call has not been allocated but
|
|
// We may fail while start monitoring
|
|
//
|
|
if ((pLineDev->DevState == DEVST_PORTLISTENINIT) ||
|
|
(pLineDev->DevState == DEVST_PORTLISTENING))
|
|
{
|
|
// Notify the monitoring application
|
|
//
|
|
pLineDev->LineClosed=TRUE;
|
|
|
|
(*pLineDev->lpfnEvent)(pLineDev->htLine, NULL, LINE_CLOSE,
|
|
0L, 0L, 0L);
|
|
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
//****************************************************************************
|
|
// DWORD DetectDialtone(PLINEDEV)
|
|
//
|
|
// Function: detects the dialtone on the phone line if the modem is capable.
|
|
// Otherwise assume there is dialtone.
|
|
//
|
|
// Returns: ERROR_SUCCESS, ERROR_IO_PENDING or an error code
|
|
//
|
|
//****************************************************************************
|
|
|
|
DWORD DetectDialtone(PLINEDEV pLineDev)
|
|
{
|
|
DWORD dwRet;
|
|
|
|
// Skip dialtone detection if Manual Dial or Blind Dial or null modem
|
|
//
|
|
if (!(GETOPTIONS(pLineDev->pDevCfg) & MANUAL_DIAL ||
|
|
pLineDev->dwDialOptions & MDM_BLIND_DIAL ||
|
|
IS_NULL_MODEM(pLineDev) ||
|
|
pLineDev->fPartialDialing == FALSE))
|
|
{
|
|
// Start the dialtone detection process
|
|
//
|
|
dwRet = UnimodemDial(pLineDev, szSemicolon, pLineDev->dwDialOptions);
|
|
}
|
|
else
|
|
{
|
|
dwRet = ERROR_SUCCESS;
|
|
};
|
|
|
|
return dwRet;
|
|
}
|
|
|
|
#ifdef UNDER_CONSTRUCTION
|
|
//****************************************************************************
|
|
// linWindowProc()
|
|
//
|
|
// Function: Private message handling proc
|
|
//
|
|
//****************************************************************************
|
|
|
|
long FAR PASCAL _loadds MdmWindowProc(hWnd, message, wParam, lParam)
|
|
HWND hWnd; /* window handle */
|
|
unsigned message; /* type of message */
|
|
WPARAM wParam; /* additional information */
|
|
LPARAM lParam; /* additional information */
|
|
{
|
|
PLINEDEV pLineDev;
|
|
|
|
switch (message)
|
|
{
|
|
//**********************************************************************
|
|
// State machine driven notification
|
|
//**********************************************************************
|
|
|
|
case WM_MDMMESSAGE:
|
|
pLineDev = (PLINEDEV)LOWORD(lParam);
|
|
|
|
MdmCompleteAsync(pLineDev, wParam, HIWORD(lParam));
|
|
break;
|
|
|
|
//**********************************************************************
|
|
// line cancellation
|
|
//**********************************************************************
|
|
|
|
case WM_MDMCANCEL:
|
|
pLineDev = (PLINEDEV)LOWORD(lParam);
|
|
|
|
// Destroy the termnal window
|
|
//
|
|
if (IS_UI_DLG_UP(pLineDev, UI_DLG_TERMINAL))
|
|
{
|
|
CloseTerminalDlg(pLineDev);
|
|
};
|
|
|
|
// Notify the caller async completion
|
|
//
|
|
if (pLineDev->dwPendingID != INVALID_PENDINGID)
|
|
{
|
|
(*gfnCompletionCallback)(pLineDev->dwPendingID, 0L);
|
|
pLineDev->dwPendingID = INVALID_PENDINGID;
|
|
pLineDev->dwPendingType = INVALID_PENDINGOP;
|
|
};
|
|
|
|
// Turn off passthrough for the preterminal.
|
|
//
|
|
if (DEVST_PORTPRETERMINAL == pLineDev->DevState)
|
|
{
|
|
UnimodemSetPassthrough(pLineDev, PASSTHROUGH_OFF);
|
|
}
|
|
UnimodemHangup(pLineDev, TRUE);
|
|
pLineDev->DevState = DEVST_DISCONNECTED;
|
|
NEW_CALLSTATE(pLineDev, LINECALLSTATE_IDLE, 0);
|
|
break;
|
|
|
|
//**********************************************************************
|
|
// Notification from PnP for a modem enabling/disabling
|
|
//**********************************************************************
|
|
|
|
case WM_DEVICECHANGE:
|
|
MdmDeviceServiceChanged((UINT)wParam, lParam);
|
|
return (DefWindowProc(hWnd, message, wParam, lParam));
|
|
|
|
//**********************************************************************
|
|
// Notification from Modem CPL for a modem installation/removal
|
|
//**********************************************************************
|
|
|
|
case WM_DEVICEINSTALL:
|
|
MdmDeviceChangeNotify((UINT)wParam, (LPSTR)lParam);
|
|
break;
|
|
|
|
//**********************************************************************
|
|
// Handle the changes in the modem status
|
|
//**********************************************************************
|
|
|
|
case WM_MDMCHANGE:
|
|
MdmDeviceChanged(wParam, lParam);
|
|
break;
|
|
|
|
//**********************************************************************
|
|
// Ring count timer
|
|
//**********************************************************************
|
|
|
|
case WM_TIMER:
|
|
pLineDev = GetCBfromHandle(MAKELONG(wParam, 0)); // Timer ID is the pLineDev
|
|
if(pLineDev)
|
|
{
|
|
DWORD tcNow = GETTICKCOUNT();
|
|
KillTimer(pLineDev->hwndLine, (UINT)pLineDev);
|
|
if (DEVST_PORTLISTENOFFER == pLineDev->DevState &&
|
|
GTC_DELTA(pLineDev->dwRingTick, tcNow) >= TO_MS_RING_SEPARATION)
|
|
{
|
|
// Timeout has expired, indicating call has stopped ringing
|
|
pLineDev->dwRingTick = 0;
|
|
pLineDev->dwRingCount = 0;
|
|
pLineDev->DevState = DEVST_PORTLISTENING;
|
|
NEW_CALLSTATE(pLineDev, LINECALLSTATE_IDLE, 0);
|
|
}
|
|
else
|
|
{
|
|
DPRINTF("WM_TIMER!");
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Fall through
|
|
//
|
|
default: /* Passes it on if unproccessed */
|
|
return (DefWindowProc(hWnd, message, wParam, lParam));
|
|
}
|
|
return (NULL);
|
|
}
|
|
|
|
//****************************************************************************
|
|
// MdmDeviceServiceChanged()
|
|
//
|
|
// Function: Handle a device change notification.
|
|
//
|
|
// Returns: SUCCESS
|
|
// PENDING
|
|
// ERROR_PORT_DISCONNECTED
|
|
//
|
|
//****************************************************************************
|
|
|
|
DWORD NEAR PASCAL MdmDeviceServiceChanged (UINT uEvent, LPARAM lParam)
|
|
{
|
|
PDEV_BROADCAST_HDR pdbHdr = (PDEV_BROADCAST_HDR)lParam;
|
|
PDEV_BROADCAST_PORT pdbp;
|
|
|
|
// Determine the event type
|
|
//
|
|
switch(uEvent)
|
|
{
|
|
case DBT_DEVICEARRIVAL:
|
|
case DBT_DEVICEREMOVECOMPLETE:
|
|
//
|
|
// Check the device type, must be a port type
|
|
//
|
|
if (pdbHdr->dbch_devicetype == DBT_DEVTYP_PORT)
|
|
{
|
|
pdbp = (PDEV_BROADCAST_PORT)pdbHdr;
|
|
MdmDeviceChangeNotify( uEvent == DBT_DEVICEARRIVAL ?
|
|
UMDM_ENABLE : UMDM_DISABLE,
|
|
(LPSTR)pdbp->dbcp_name);
|
|
};
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
};
|
|
return SUCCESS;
|
|
}
|
|
#endif // UNDER_CONSTRUCTION
|