text: /****************************************************************************/
  // mcsapi.c
  //
  // TS RDPWSX MCS user-mode multiplexing layer code.
  //
  // Copyright (C) 1997-2000 Microsoft Corporation
  /****************************************************************************/
  
  #include "precomp.h"
  #pragma hdrstop
  
  #include "mcsmux.h"
  
  
  /*
   * Defines
   */
  #define DefaultNumDomains  10
  #define DefaultNumChannels 3
  
  #define MinDomainControlMarker    0xFFFFFFF0
  #define ReadChannelRequestMarker  0xFFFFFFFD
  #define CloseChannelRequestMarker 0xFFFFFFFE
  #define ExitIoThreadMarker        0xFFFFFFFF
  
  // Defines the maximum number of threads that can be waiting on the global
  // I/O completion port. 0 means the same as the number of processors in
  // the system.
  #define MaxIoPortThreads 0
  
  
  /*
   * DLL globals
   */
  static PVOID      g_NCUserDefined = NULL;
  static DWORD      g_IoThreadID = 0;
  static HANDLE     g_hIoPort = NULL,
                    g_hIoThread = NULL,
                    g_hIoThreadEndEvent = NULL;
  static OVERLAPPED g_NCOverlapped;
  static MCSNodeControllerCallback g_NCCallback = NULL;
  
  // Externs global to all sources.
  BOOL  g_bInitialized = FALSE;
  
  #ifdef MCS_FUTURE
  CRITICAL_SECTION g_csGlobalListLock;
  SList g_UserList;
  SList g_ConnList;
  #endif
  
  
  /*
   * Local function prototypes.
   */
  MCSError SendConnectProviderResponse(Domain *, ConnectionHandle, MCSResult,
          BYTE *, unsigned);
  
  
  /*
   * Initialization done at RDPWSX.dll load.
   */
  BOOL MCSDLLInit(void)
  {
      return TRUE;
  }
  
  
  /*
   * Cleanup done at RDPWSX.dll unload.
   */
  void MCSDllCleanup(void)
  {
      // Cleanup all resources. MCSCleanup() must handle abnormal
      //   terminations where MCSCleanup() was not properly called
      //   by the node controller.
      MCSCleanup();
  }
  
  
  /*
   * Utility functions.
   */
  void DestroyUserInfo(UserInformation *pUserInfo)
  {
      MCSChannel *pMCSChannel;
      
      SListResetIteration(&pUserInfo->JoinedChannelList);
      while (SListIterate(&pUserInfo->JoinedChannelList,
              (unsigned *)&pMCSChannel, &pMCSChannel))
          Free(pMCSChannel);
      SListDestroy(&pUserInfo->JoinedChannelList);
  }
  
  
  // Perform common domain destruction. Used in multiple places in code.
  void DestroyDomain(Domain *pDomain)
  {
      // Fill the to-be-freed Domain with recognizable trash
      // and then release it back to the heap
      DeleteCriticalSection(&pDomain->csLock);
      Free(pDomain);
  
  }
  
  
  /*
   * Handles a connect-provider indication ChanelInput coming from kernel mode.
   */
  void HandleConnectProviderIndication(
          Domain                         *pDomain,
          unsigned                       BytesTransferred,
          ConnectProviderIndicationIoctl *pCPin)
  {
      MCSError MCSErr;
      Connection *pConn;
      ConnectProviderIndication CP;
  
      if (BytesTransferred != sizeof(ConnectProviderIndicationIoctl)) {
          ErrOutIca1(pDomain->hIca, "HandleConnectProvInd(): Wrong size data "
                  "received (%d), ignoring", BytesTransferred);
          return;
      }
  
      ASSERT(pCPin->UserDataLength <= MaxGCCConnectDataLength);
  
      if (pDomain->State != Dom_Unconnected) {
          ErrOutIca(pDomain->hIca, "HandleConnectProvInd(): Connect received "
                  "unexpectedly, ignoring");
          return;
      }
  
      // Generate a new Connection for user mode. Associate it with the domain.
      // This allows the ConnectProviderResponse() to find the domain again.
      // TODO FUTURE: We use a static Connection embedded in the Domain since
      //   we are currently a single-connection system. Change this in the
      //   future for multiple-connection system.
      pConn = &pDomain->MainConn;
      pConn->pDomain = pDomain;
      pConn->hConnKernel = pCPin->hConn;
  
  #ifdef MCS_Future
      pConn = Malloc(sizeof(Connection));
      if (pConn == NULL) {
          ErrOutIca(pDomain->hIca, "HandleConnectProvInd(): Could not "
                  "create Connection");
  
          // Send error PDU back.
          MCSErr = SendConnectProviderResponse(pDomain, pCPin->hConn,
                  RESULT_UNSPECIFIED_FAILURE, NULL, 0);
  
          // We cannot do much more error handling if this does not work.
          ASSERT(MCSErr == MCS_NO_ERROR);
          return;
      }
  
      EnterCriticalSection(&g_csGlobalListLock);
      if (!SListAppend(&g_ConnList, (unsigned)pConn, pDomain)) {
          LeaveCriticalSection(&g_csGlobalListLock);
  
          ErrOutIca(pDomain->hIca, "ConnectProvInd: Could not "
                  "add hConn to global list");
  
          // Send error PDU back.
          MCSErr = SendConnectProviderResponse(pDomain, pCPin->hConn,
                  RESULT_UNSPECIFIED_FAILURE, NULL, 0);
  
          // We cannot do much more error handling if this does not work.
          ASSERT(MCSErr == MCS_NO_ERROR);
          return;
      }
      LeaveCriticalSection(&g_csGlobalListLock);
  #endif
  
      // Store information away for future use.
      pDomain->DomParams = pCPin->DomainParams;
  
      // Prepare a ConnectProviderIndication for sending up to
      //   the node controller.
      CP.hConnection = pConn;
      CP.bUpwardConnection = pCPin->bUpwardConnection;
      CP.DomainParams = pCPin->DomainParams;
  
      CP.UserDataLength = pCPin->UserDataLength;
      if (CP.UserDataLength == 0)
          CP.pUserData = NULL;
      else
          CP.pUserData = pCPin->UserData;
  
      //TODO FUTURE: This is a hack, assumes only one connection
      //   per domain. This is used in DisconnectProviderInd
      //   to get the user mode hConn for the domain.
      pDomain->hConn = pConn;
  
      // Set state to pending response.
      pDomain->State = Dom_PendingCPResponse;
  
      // Call the node controller callback.
      TraceOutIca(pDomain->hIca, "MCS_CONNECT_PROV_IND received, calling node "
              "ctrl callback");
      ASSERT(g_NCCallback != NULL);
      g_NCCallback(pDomain, MCS_CONNECT_PROVIDER_INDICATION,
              &CP, pDomain->NCUserDefined);
  }
  
  
  /*
   * Handles a disconnect-provider indication ChanelInput coming from kernel
   *   mode.
   */
  void HandleDisconnectProviderIndication(
          Domain                            *pDomain,
          unsigned                          BytesTransferred,
          DisconnectProviderIndicationIoctl *pDPin)
  {
      Domain *pDomainConn;
      Connection *pConn;
      DisconnectProviderIndication DP;
  
      if (BytesTransferred != sizeof(DisconnectProviderIndicationIoctl)) {
          ErrOutIca1(pDomain->hIca, "HandleDiscProvInd(): Wrong size data "
                  "received (%d), ignoring", BytesTransferred);
          return;
      }
  
  #ifdef MCS_Future
      // Remove the connection from the connection list, destroy.
      EnterCriticalSection(&g_csGlobalListLock);
      SListResetIteration(&g_ConnList);
      while (SListIterate(&g_ConnList, (unsigned *)&pConn, &pDomainConn)) {
          if (pConn->hConnKernel == pDPin->hConn) {
              ASSERT(pDomainConn == pDomain);
              SListRemove(&g_ConnList, (unsigned)pConn, NULL);
  
              // TODO FUTURE: This was removed to work with the statically
              //   allocated Connection object contained in the Domain.
              //   Restore if moving to a multiple-connection system.
              Free(pConn);
  
              break;
          }
      }
      LeaveCriticalSection(&g_csGlobalListLock);
  #endif
  
      // We are now no longer connected.
      pDomain->hConn = NULL;
      pDomain->State = Dom_Unconnected;
  
      // Prepare a DisconnectProviderIndication for sending up
      //   to node controller.
      DP.hDomain = pDomain;
      //TODO FUTURE: This hack assumes only one connection per
      //    domain.
      DP.hConnection = pDomain->hConn;
      DP.Reason = pDPin->Reason;
  
      // Call the node controller callback.
      TraceOutIca(pDomain->hIca, "MCS_DISCONNECT_PROV_IND received, calling "
              "node ctrl callback");
      ASSERT(g_NCCallback != NULL);
      g_NCCallback(pDomain, MCS_DISCONNECT_PROVIDER_INDICATION, &DP,
              pDomain->NCUserDefined);
  }
  
  
  /*
   * Takes a reference count on a domain
   */
  VOID MCSReferenceDomain(Domain *pDomain)
  {
      if (InterlockedIncrement(&pDomain->RefCount) <= 0)
          ASSERT(0);
  }
  
  
  /*
   * Releases domain resources if the reference count goes to zero
   */
  VOID MCSDereferenceDomain(Domain *pDomain)
  {
      ASSERT(pDomain->RefCount > 0);
  
      // Don't delete the domain unless everyone is done with it.  This means
      // GCC has let it go, and there are no pending I/O's for it.
      if (InterlockedDecrement(&pDomain->RefCount) == 0) {
          DestroyDomain(pDomain);
      }
  }
  
  
  /*
   * Closes the domain channel.  Does nothing if the channel is already closed
   */
  VOID MCSChannelClose(Domain *pDomain)
  {
      // Note that the channel is disconnected, and then
      // close the ica channel if it is still open
  
      pDomain->bPortDisconnected = TRUE;
  
      if (pDomain->hIcaT120Channel != NULL)
      {
          //TraceOutIca(pDomain->hIca, "MCSChannelClose(): Closing "
          //        "T120 ICA channel");
          
          CancelIo(pDomain->hIcaT120Channel);
          IcaChannelClose(pDomain->hIcaT120Channel);
  
          pDomain->hIcaT120Channel = NULL;
      }
  }
  
  
  /*
   * Initiates port disconnection
   */
  NTSTATUS MCSDisconnectPort(Domain       *pDomain,
                             MCSReason    Reason)
  {
      NTSTATUS                        ntStatus = STATUS_SUCCESS;
      DisconnectProviderRequestIoctl  DPrq;
      
      // Send special disconnect-provider request to kernel to trigger
      //   sending detach-user requests to local attachments with their own
      //   UserIDs, signaling that the domain is going away.
  
      if (!pDomain->bPortDisconnected) {
          DPrq.Header.Type = MCS_DISCONNECT_PROVIDER_REQUEST;
          DPrq.Header.hUser = NULL;           // Special meaning node controller.
          DPrq.hConn = NULL;                  // Special meaning last local connection.
          DPrq.Reason = Reason;
      
          // Call kernel mode
      
          ntStatus = IcaStackIoControl(pDomain->hIcaStack, IOCTL_T120_REQUEST, &DPrq,
                                       sizeof(DPrq), NULL, 0, NULL);
     
      }
          
      // Queue a channel close request to the IoThreadFunc() to cancel the I/O
      // since GCC is done with it.  This is necessary as the I/O must
      // be canceled from the same thread that initially issued it.
      MCSReferenceDomain(pDomain);
      PostQueuedCompletionStatus(g_hIoPort, CloseChannelRequestMarker,
              (ULONG_PTR)pDomain, NULL);
  
      return ntStatus;
  }
  
  
  /*
   * Handles port data and reissues the read
   */
  VOID MCSPortData(Domain *pDomain,
                   DWORD   BytesTransferred)
  {
      IoctlHeader *pHeader;
      
      EnterCriticalSection(&pDomain->csLock);
      
      // If real data has been received on the channel instead of a queued domain
      // control message, then process the data.
      if (BytesTransferred < MinDomainControlMarker) {
          
          // We only do callbacks if MCSDeleteDomain() was not called.
          if (pDomain->bDeleteDomainCalled == FALSE) {
              // Decode the ChannelInput and make the callback.
              
              pHeader = (IoctlHeader *)pDomain->InputBuf;
      
              switch (pHeader->Type)
              {
                  case MCS_CONNECT_PROVIDER_INDICATION:
                      ASSERT(pHeader->hUser == NULL);
      
                      HandleConnectProviderIndication(pDomain,
                              BytesTransferred,
                              (ConnectProviderIndicationIoctl *)pHeader);
                      break;
      
                  case MCS_DISCONNECT_PROVIDER_INDICATION:
                      ASSERT(pHeader->hUser == NULL);
      
                      HandleDisconnectProviderIndication(pDomain,
                              BytesTransferred,
                              (DisconnectProviderIndicationIoctl *)pHeader);
                      MCSChannelClose(pDomain);
                      break;
      
                  default:
                      //TODO FUTURE: Handle other MCS indications/confirms.
                      ErrOutIca2(pDomain->hIca, "IoThreadFunc(): Unknown "
                              "node controller ioctl %d received for "
                              "domain %X", pHeader->Type, pDomain);
                      break;
              }
              
              // Set the message number to an invalid value.
              // This makes sure that any improperly-received dequeued
              //   messages that do not bring data with them will, when
              //   reusing pDomain->InputBuf, fall to the default part of
              //   the switch statement and throw an error.
      
              pHeader->Type = 0xFFFFFFFF;
          }        
      }
  
      // Else, a special domain control request has been queued to the I/O port
      else {
          switch (BytesTransferred) {
              case ReadChannelRequestMarker :
                  break;
  
              case CloseChannelRequestMarker :
                  MCSChannelClose(pDomain);
                  break;
  
              default:
                  ErrOutIca2(pDomain->hIca, "MCSPortData: Unknown domain control "
                             "for Domain(%lx), code(%lx)",
                             pDomain, BytesTransferred);
          }
      }
  
      // Issue a new read to catch the next indication/confirm.  Overlapped 
      // I/O reads will return as "unsuccessful" if there is no data already
      // waiting to be read so check for that status specifically.
      if (pDomain->hIcaT120Channel) {
          if (ReadFile(pDomain->hIcaT120Channel, pDomain->InputBuf,
                  DefaultInputBufSize, NULL, &pDomain->Overlapped) ||
              (GetLastError() == ERROR_IO_PENDING))
              MCSReferenceDomain(pDomain);
          else
              {
                  // Warning only. This should occur only when the ICA stack
                  //   is being torn down without our direct knowledge. In that
                  //   case, we simply keep running until we are torn down at the
                  //   user mode level.  Take off a ref count since there is no
                  //   longer a pending I/O.
                  WarnOutIca2(pDomain->hIca, "IoThreadFunc(): Could not perform "
                          "ReadFile, pDomain=%X, rc=%X", pDomain, GetLastError());
              }
      }  
  
      // Release the lock on the domain
      LeaveCriticalSection(&pDomain->csLock);
  
      // drop the reference count since we just completed processing
      MCSDereferenceDomain(pDomain);
  }
  
  
  /*
   * Param is the handle for the I/O completion port to wait on.
   * Completion key of ExitIoThreadMarker tells the thread to exit.
   * On exit we set an event to indicate we are done. This must be done instead
   *   of relying solely on the thread's handle signaling in the case where
   *   the unload is occurring because of abnormal termination, and Cleanup()
   *   does not get called normally but instead from within DLL_PROCESS_DETACH.
   *   The call to DllEntryPoint prevents the thread handle from signaling,
   *   so we would end up in a race condition. A parallel event handle can be
   *   signaled correctly in all cases.
   */
  
  DWORD WINAPI IoThreadFunc(void *Param)
  {
      BOOL        bSuccess;
      DWORD       BytesTransferred;
      Domain      *pDomain;
      OVERLAPPED  *pOverlapped;
  
      ASSERT(Param != NULL);
  
      for (;;) 
      {
          // Wait for a port completion status
          
          pDomain = NULL;
          pOverlapped = NULL;
          bSuccess = GetQueuedCompletionStatus((HANDLE)Param, &BytesTransferred,
                  (ULONG_PTR *)&pDomain, &pOverlapped, INFINITE);
  
          // Check for failed dequeue, at which point pDomain is not valid.
          if (!bSuccess && (pOverlapped == NULL)) {
              continue;
          }
  
          // If the pDomain is ExitIoThreadMarker then we are
          // shutting down and being unloaded
          if (pDomain == (Domain *)(DWORD_PTR)ExitIoThreadMarker)
              break;
  
          // We have successfully received a completion status.  Either it is a 
          // domain control request or user data
          if (bSuccess)
              MCSPortData(pDomain, BytesTransferred);
          
          // Else a cancel or abort I/O has occurred.  Release a ref count since
          // an I/O is no longer pending.
          else
              MCSDereferenceDomain(pDomain);
      }
  
      ASSERT(g_hIoThreadEndEvent);
      SetEvent(g_hIoThreadEndEvent);
  
      return 0;
  }
  
  
  /*
   * MUX-only non-MCS primitive function to allow ICA code to inject a new entry
   *   into the MUX-internal domain/stack database.
   */
  MCSError APIENTRY MCSCreateDomain(
          HANDLE hIca,
          HANDLE hIcaStack,
          void *pContext,
          DomainHandle *phDomain)
  {
      NTSTATUS status;
      Domain *pDomain;
      NTSTATUS Status;
      IoctlHeader StartIoctl;
  
      CheckInitialized("CreateDomain()");
  
      // Init the receiver's data to NULL to ensure a fault if they skip the
      // error code.
      *phDomain = NULL;
  
      pDomain = Malloc(sizeof(Domain));
      if (pDomain != NULL) {
          // Create and enter the locking critical section.
          status = RtlInitializeCriticalSection(&pDomain->csLock);
          if (status == STATUS_SUCCESS) {
              EnterCriticalSection(&pDomain->csLock);
  
  #if MCS_Future
              pDomain->SelLen = 0;
  #endif
  
              pDomain->hIca = hIca;
              pDomain->hIcaStack = hIcaStack;
              pDomain->NCUserDefined = pContext;
              pDomain->State = Dom_Unconnected;
              pDomain->Overlapped.hEvent = NULL;
              pDomain->Overlapped.Offset = pDomain->Overlapped.OffsetHigh = 0;
              pDomain->RefCount = 0;
  
              // Take a reference count for the caller of this function (GCC).
              MCSReferenceDomain(pDomain);
  
              // Open T.120 ICA virtual channel.
              Status = IcaChannelOpen(hIca, Channel_Virtual, Virtual_T120,
                      &pDomain->hIcaT120Channel);
              if (!NT_SUCCESS(Status)) {
                  ErrOutIca(hIca, "CreateDomain: Error opening virtual channel");
                  goto PostInitCS;
              }
  
              // Add the hIcaT120 Channel to the handles associated with the
              // main I/O completion port. We use pDomain as the completion key
              // so we can find it during callback processing.
              if (CreateIoCompletionPort(pDomain->hIcaT120Channel, g_hIoPort,
                      (ULONG_PTR)pDomain, MaxIoPortThreads) == NULL) {
                  ErrOutIca(hIca, "CreateDomain(): Could not add ICA channel to "
                          "I/O completion port");
                  goto PostChannel;
              }
  
              // Tell kernel mode to start the MCS I/O.
              StartIoctl.hUser = NULL;
              StartIoctl.Type = MCS_T120_START;
              Status = IcaStackIoControl(hIcaStack, IOCTL_T120_REQUEST,
                      &StartIoctl, sizeof(StartIoctl), NULL, 0, NULL);
              if (!NT_SUCCESS(Status)) {
                  ErrOutIca(hIca, "Could not start kernel T120 I/O");
                  goto PostChannel;
              }
  
              // Set the domain handle for use by GCC
              *phDomain = pDomain;
  
              // Send a message to the IoThreadFunc to initiate the read on
              // the channel. We do this so that the same thread is always
              // responsible for all I/O operations. This is important for
              // CancelIo. Take another ref count since an I/O result is
              // pending for the completion port now.
              MCSReferenceDomain(pDomain);
              PostQueuedCompletionStatus(g_hIoPort, ReadChannelRequestMarker,
                      (ULONG_PTR)pDomain, NULL);
  
              // Leave the Domain locking critical section.
              LeaveCriticalSection(&pDomain->csLock);
          }
          else {
              ErrOutIca(hIca, "CreateDomain: Error initialize pDomain->csLock");
              goto PostAlloc;
          }
      }
      else {
          ErrOutIca(hIca, "CreateDomain(): Error allocating a new domain");
          return MCS_ALLOCATION_FAILURE;
      }
  
      return MCS_NO_ERROR;
  
  
  // Error handling
  
  PostChannel:
      IcaChannelClose(pDomain->hIcaT120Channel);
  
  PostInitCS:
      LeaveCriticalSection(&pDomain->csLock);
      DeleteCriticalSection(&pDomain->csLock);
  
  PostAlloc:
      Free(pDomain);
  
      return MCS_ALLOCATION_FAILURE;
  }
  
  
  
  /*
   * Signals that an hIca is no longer valid.
   * Tears down the associated domain, including sending detach-user
   *    indications to remaining local attachments.
   */
  
  MCSError APIENTRY MCSDeleteDomain(
          HANDLE       hIca,
          DomainHandle hDomain,
          MCSReason    Reason)
  {
      Domain *pDomain, pDomainConn;
      NTSTATUS Status;
      MCSError MCSErr;
      Connection *pConn;
  
      pDomain = (Domain *)hDomain;
  
      TraceOutIca(hIca, "DeleteDomain() entry");
  
      // Gain access to the domain. Prevents collision with other API functions
      //   and concurrent callbacks.
      EnterCriticalSection(&pDomain->csLock);
  
      // This API should not be called more than once per domain.
      if (pDomain->bDeleteDomainCalled) {
          ASSERT(!pDomain->bDeleteDomainCalled);
          MCSErr = MCS_INVALID_PARAMETER;
          LeaveCriticalSection(&pDomain->csLock);
          goto PostLockDomain;
      }
  
  #ifdef MCS_Future
      // If we are still connected find and remove remaining connections
      //   which refer to this domain.
      if (pDomain->State != Dom_Unconnected) {
          //TODO FUTURE: This assumes only one connection per domain.
          EnterCriticalSection(&g_csGlobalListLock);
          SListRemove(&g_ConnList, (unsigned)pDomain->hConn,
                  (void **)&pDomainConn);
          LeaveCriticalSection(&g_csGlobalListLock);
  
          // TODO FUTURE: This was removed to work with the statically
          //   allocated Connection object contained in the Domain.
          //   Restore if moving to a multiple-connection system.
          if (pDomainConn != NULL)
              Free(pConn);
      }
  #endif
  
      pDomain->hConn = NULL;
      pDomain->State = Dom_Unconnected;
  
      //TODO: Implement detach-user indications for local attachments.
  
      // Queue the "Destroy this domain!" request. This allows the domain
      // free code to be in only one place and the IoPort queue will
      // serialize this request along with closed virtual channel indications.
  
      pDomain->bDeleteDomainCalled = TRUE;
      MCSDisconnectPort(pDomain, Reason);
  
      // Drop a ref count because GCC is done with the domain.  This count was
      // originally incremented for GCC in MCSCreateDomain().
      LeaveCriticalSection(&pDomain->csLock);
      MCSDereferenceDomain(pDomain);
      MCSErr = MCS_NO_ERROR;
  
  PostLockDomain:
      
      return MCSErr;
  }
  
  
  
  /*
   * Called by node controller to initialize DLL.
   */
  
  MCSError APIENTRY MCSInitialize(MCSNodeControllerCallback Callback)
  {
      NTSTATUS status; 
  
  #if DBG
      if (g_bInitialized) {
          ErrOut("Initialize() called when already initialized");
          return MCS_ALREADY_INITIALIZED;
      }
  #endif
  
      // Initialize node controller specific information.
      g_NCCallback = Callback;
  
  #ifdef MCS_FUTURE
      // Initialize global MCS lists.
      status = RtlInitializeCriticalSection(&g_csGlobalListLock);
      if (status != STATUS_SUCCESS) {
          ErrOut("MCSInitialize: Error initialize g_csGlobalListLock");
          return MCS_ALLOCATION_FAILURE;
      }
      EnterCriticalSection(&g_csGlobalListLock);
      SListInit(&g_UserList, DefaultNumDomains);
      SListInit(&g_ConnList, DefaultNumDomains);
      LeaveCriticalSection(&g_csGlobalListLock);
  #endif
  
      // Create I/O completion port, not associated with any requests.
      g_hIoPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0,
              MaxIoPortThreads);
      if (g_hIoPort == NULL) {
          ErrOut1("IO completion port not created (rc = %ld)", GetLastError());
          return MCS_ALLOCATION_FAILURE;
      }
  
      // Create I/O port listening thread, starts waiting immediately.
      g_hIoThread = CreateThread(NULL, 0, IoThreadFunc, g_hIoPort, 0,
              &g_IoThreadID);
      if (g_hIoThread == NULL) {
          ErrOut1("IO thread not created (rc = %ld)", GetLastError());
          return MCS_ALLOCATION_FAILURE;
      }
  
      // Increase the priority of the IoThread to increase connect performance.
      SetThreadPriority(g_hIoThread, THREAD_PRIORITY_HIGHEST);
  
      g_bInitialized = TRUE;
  
      return MCS_NO_ERROR;
  }
  
  
  
  /*
   * Called by the node controller or DllEntryPoint() to shut down the DLL.
   */
  
  MCSError APIENTRY MCSCleanup(void)
  {
      DWORD WaitResult;
      Domain *pDomain;
      Connection *pConn;
      UserInformation *pUserInfo;
  
      CheckInitialized("Cleanup()");
  
      if (!g_bInitialized)
          return MCS_NO_ERROR;
          
      // Kill I/O completion port, thread(s) waiting on it.
      g_hIoThreadEndEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
      PostQueuedCompletionStatus(g_hIoPort, 0, ExitIoThreadMarker, NULL);
  
      WaitResult = WaitForSingleObject(g_hIoThreadEndEvent, INFINITE);
  
      CloseHandle(g_hIoThreadEndEvent);
      CloseHandle(g_hIoThread);
      CloseHandle(g_hIoPort);
  
      // Cleanup node controller-specific information.
      g_NCCallback = NULL;
      g_NCUserDefined = NULL;
  
      /*
       * Clean up global MCS lists.
       */
  
  #ifdef MCS_Future
      EnterCriticalSection(&g_csGlobalListLock);
  
      // Kill remaining users.
      SListResetIteration(&g_UserList);
      while (SListIterate(&g_UserList, (unsigned *)&pUserInfo, NULL)) {
          DestroyUserInfo(pUserInfo);
          Free(pUserInfo);
      }
      SListDestroy(&g_UserList);
  
      // Kill remaining connections.
      SListResetIteration(&g_ConnList);
      // TODO FUTURE: Removed to work with statically allocated Connection
      //   contained in Domain. Restore fo multiple-connection system.
      while (SListIterate(&g_ConnList, (unsigned *)&pConn, NULL))
          Free(pConn);
      SListDestroy(&g_ConnList);
  
      LeaveCriticalSection(&g_csGlobalListLock);
      DeleteCriticalSection(&g_csGlobalListLock);
  #endif
  
      g_bInitialized = FALSE;
  
      return MCS_NO_ERROR;
  }
  
  
  /*
   * Response function for an MCS_CONNECT_PROVIDER_INDICATION callback. Result
   *   values are as defined by T.125.
   */
  
  MCSError APIENTRY MCSConnectProviderResponse(
          DomainHandle hDomain,
          MCSResult    Result,
          BYTE         *pUserData,
          unsigned     UserDataLen)
  {
      Domain *pDomain;
      MCSError MCSErr;
      ConnectProviderResponseIoctl CPrs;
      
      CheckInitialized("ConnectProvResponse()");
  
      pDomain = (Domain *) hDomain;
  
      // Lock the domain.
      EnterCriticalSection(&pDomain->csLock);
  
      // It is possible for us to call this function in the wrong state, e.g.
      //   if we have gotten a disconnect very soon after processing a
      //   connection, and have transitioned to State_Unconnected.
      if (pDomain->State != Dom_PendingCPResponse) {
          TraceOutIca(pDomain->hIca, "ConnectProvderResp(): in wrong state, "
                  "ignoring call");
          MCSErr = MCS_NO_ERROR;
          goto PostLockDomain;
      }
  
      // TODO FUTURE: We are assuming an hConn in the Domain which is a hack
      //   only for a single-connection system.
      MCSErr = SendConnectProviderResponse(pDomain,
              ((Connection *)pDomain->hConn)->hConnKernel,
              Result, pUserData, UserDataLen);
      if (MCSErr == MCS_NO_ERROR) {
          if (Result == RESULT_SUCCESSFUL)
              pDomain->State = Dom_Connected;
          else
              pDomain->State = Dom_Rejected;
      }
  
  PostLockDomain:
      // Release the domain.
      LeaveCriticalSection(&pDomain->csLock);
  
      return MCSErr;
  }
  
  // Utility function called internally as well as part of
  //   MCSConnectProviderResponse(). Domain must be locked before calling.
  MCSError SendConnectProviderResponse(
          Domain           *pDomain,
          ConnectionHandle hConnKernel,
          MCSResult        Result,
          BYTE             *pUserData,
          unsigned         UserDataLen)
  {
      NTSTATUS Status;
      ConnectProviderResponseIoctl CPrs;
  
      // Transfer params.
      CPrs.Header.hUser = NULL;  // Special value meaning node controller.
      CPrs.Header.Type = MCS_CONNECT_PROVIDER_RESPONSE;
      CPrs.hConn = hConnKernel;
      CPrs.Result = Result;
      CPrs.UserDataLength = UserDataLen;
  
      // Points to user data.
      if (UserDataLen)
          CPrs.pUserData = pUserData;
  
      // Call kernel mode. No callback is expected.
      Status = IcaStackIoControl(pDomain->hIcaStack, IOCTL_T120_REQUEST, &CPrs,
              sizeof(ConnectProviderResponseIoctl), NULL, 0, NULL);
      if (!NT_SUCCESS(Status)) {
          ErrOutIca(pDomain->hIca, "ConnectProvResponse(): Stack ioctl failed");
          return MCS_ALLOCATION_FAILURE;
      }
      TraceOutIca(pDomain->hIca, "Sent MCS_CONNECT_PROVIDER_RESPONSE");
  
      return MCS_NO_ERROR;
  }
  
  
  
  /*
   * Terminates an existing MCS connection or aborts a creation in-progress.
   */
  
  MCSError APIENTRY MCSDisconnectProviderRequest(
          HANDLE hIca,
          ConnectionHandle hConn,
          MCSReason Reason)
  {
      Domain *pDomain;
      NTSTATUS Status;
      MCSError MCSErr;
      Connection *pConn;
      DisconnectProviderRequestIoctl DPrq;
  
      CheckInitialized("DisconnectProviderReq()");
  
      pConn = (Connection *)hConn;
      pDomain = pConn->pDomain;
  
      // Lock the domain.
      EnterCriticalSection(&pDomain->csLock);
  
      // Transfer params.
      DPrq.Header.hUser = NULL;  // Special meaning node controller.
      DPrq.Header.Type = MCS_DISCONNECT_PROVIDER_REQUEST;
      DPrq.hConn = pConn->hConnKernel;
      DPrq.Reason = Reason;
  
      // TODO FUTURE: Assumes only one connection.
      pDomain->hConn = NULL;
      pDomain->State = Dom_Unconnected;
  
      // TODO FUTURE: We do not deallocate the Connection. Unnecessary for now
      //   since we are using a static Connection in the Domain, but will be an
      //   issue when mallocing the Connection objects.
  #ifdef MCS_Future
      EnterCriticalSection(&g_csGlobalListLock);
      SListRemove(&g_ConnList, (unsigned)pConn, NULL);
      LeaveCriticalSection(&g_csGlobalListLock);
  
      Free(pConn);
  #endif
  
      // Call kernel mode. No callback is expected.
      Status = IcaStackIoControl(pDomain->hIcaStack, IOCTL_T120_REQUEST, &DPrq,
              sizeof(DPrq), NULL, 0, NULL);
      if (!NT_SUCCESS(Status)) {
          ErrOutIca(hIca, "DisconnectProvRequest(): Stack ioctl failed");
          MCSErr = MCS_ALLOCATION_FAILURE;
          goto PostLockDomain;
      }
      
      TraceOutIca(hIca, "Sent MCS_DISCONNECT_PROVIDER_REQUEST");
  
      MCSErr = MCS_NO_ERROR;
  
  PostLockDomain:
      // Release the domain.
      LeaveCriticalSection(&pDomain->csLock);
  
      return MCSErr;
  }
  
  /*
   *
   * CODE PAST THIS POINT IS FOR REFERENCE PURPOSES ONLY.  IT IS NOT PART OF
   * THE PRODUCT.
   *
   */
  
  #if MCS_Future
  /*
   * Connects a local domain to a remote domain, merging the two domains.
   */
  
  MCSError APIENTRY MCSConnectProviderRequest(
          DomainSelector    CallingDom,
          unsigned          CallingDomLen,
          DomainSelector    CalledDom,
          unsigned          CalledDomLen,
          BOOL              bUpwardConnection,
          PDomainParameters pDomParams,
          BYTE              *UserData,
          unsigned          UserDataLen,
          DomainHandle      *phDomain,
          ConnectionHandle  *phConn)
  
  {
      CheckInitialized("ConnectProvReq()");
  
  // Create new domain or look up already-created one.
  // call to kernel mode to set up the connection.
  
      ErrOut("ConnectProviderRequest: Not implemented");
      return MCS_COMMAND_NOT_SUPPORTED;
  }
  #endif  // MCS_Future
  
  
  #if MCS_Future
  /*
   * Attaches a user SAP to an existing domain.
   */
  
  MCSError APIENTRY MCSAttachUserRequest(
          DomainHandle        hDomain,
          MCSUserCallback     UserCallback,
          MCSSendDataCallback SDCallback,
          void                *UserDefined,
          UserHandle          *phUser,
          unsigned            *pMaxSendSize,
          BOOLEAN             *pbCompleted)
  {
      Domain *pDomain;
      unsigned i, Err, OutBufSize, BytesReturned;
      NTSTATUS ntStatus;
      UserInformation *pUserInfo;
      AttachUserReturnIoctl AUrt;
      AttachUserRequestIoctl AUrq;
  
      CheckInitialized("AttachUserReq()");
  
      pDomain = (Domain *)hDomain;
      *pbCompleted = FALSE;
  
      // We must by this time have an hICA assigned to the domain.
      ASSERT(pDomain->hIca != NULL);
  
      // Make a new user-mode user information struct.
      *phUser = pUserInfo = Malloc(sizeof(UserInformation));
      if (pUserInfo == NULL) {
          ErrOutIca(pDomain->hIca, "AttachUserReq(): Alloc failure for "
                     "user info");
          return MCS_ALLOCATION_FAILURE;
      }
  
      // Fill in the struct
      pUserInfo->Callback = UserCallback;
      pUserInfo->SDCallback = SDCallback;
      pUserInfo->UserDefined = UserDefined;
      pUserInfo->State = User_AttachConfirmPending;
      pUserInfo->hUserKernel = NULL;  // Don't yet have kernel hUser assignment.
      pUserInfo->UserID = 0;  // Don't yet have kernel UserID assignment.
      pUserInfo->pDomain = pDomain;
      SListInit(&pUserInfo->JoinedChannelList, DefaultNumChannels);
  
      // Transfer params for kernel-mode call.
      AUrq.Header.hUser = NULL;
      AUrq.Header.Type = MCS_ATTACH_USER_REQUEST;
      AUrq.UserDefined = UserDefined;
  
      // Add the user to the global UserInfo list.
      EnterCriticalSection(&g_csGlobalListLock);
      if (!SListAppend(&g_UserList, (unsigned)pUserInfo, pDomain)) {
          ErrOutIca(pDomain->hIca, "AttachUserReq(): Could not add user to "
                     "user list");
          AUrt.MCSErr = MCS_ALLOCATION_FAILURE;
          LeaveCriticalSection(&g_csGlobalListLock);
          goto PostAlloc;
      }
      LeaveCriticalSection(&g_csGlobalListLock);
  
      // Issue the T120 request to kernel mode.
      ntStatus = IcaStackIoControl(pDomain->hIcaStack, IOCTL_T120_REQUEST,
              &AUrq, sizeof(AUrq), &AUrt, sizeof(AUrt), &BytesReturned);
      if (!NT_SUCCESS(ntStatus)) {
          ErrOutIca(pDomain->hIca, "AttachUserRequest(): T120 request failed");
          AUrt.MCSErr = MCS_ALLOCATION_FAILURE;
          goto PostAddList;
      }
      if (AUrt.MCSErr != MCS_NO_ERROR)
          goto PostAddList;
  
      pUserInfo->hUserKernel = AUrt.hUser;
      *phUser = pUserInfo;
      *pMaxSendSize = AUrt.MaxSendSize;
      pUserInfo->MaxSendSize = AUrt.MaxSendSize;
      if (AUrt.bCompleted) {
          pUserInfo->UserID = AUrt.UserID;
          *pbCompleted = TRUE;
      }
  
      return MCS_NO_ERROR;
  
  // Error handling.
  PostAddList:
      EnterCriticalSection(&g_csGlobalListLock);
      SListRemove(&g_UserList, (unsigned)pUserInfo, NULL);
      LeaveCriticalSection(&g_csGlobalListLock);
      
  PostAlloc:
      SListDestroy(&pUserInfo->JoinedChannelList);
      Free(pUserInfo);
      
      return AUrt.MCSErr;
  }
  #endif  // MCS_Future
  
  
  
  #if MCS_Future
  UserID MCSGetUserIDFromHandle(UserHandle hUser)
  {
      return ((UserInformation *)hUser)->UserID;
  }
  #endif // MCS_Future
  
  
  
  #if MCS_Future
  /*
   * Detach a previously created user attachment from a domain.
   */
  
  MCSError APIENTRY MCSDetachUserRequest(UserHandle hUser)
  {
      Domain *pDomain;
      NTSTATUS Status;
      unsigned BytesReturned;
      MCSError MCSErr;
      MCSChannel *pMCSChannel;
      UserInformation *pUserInfo;
      DetachUserRequestIoctl DUrq;
  
      CheckInitialized("DetachUserReq()");
      
      pUserInfo = (UserInformation *)hUser;
      
      // Fill in detach-user request for sending to kernel mode.
      DUrq.Header.Type = MCS_DETACH_USER_REQUEST;
      DUrq.Header.hUser = pUserInfo->hUserKernel;
  
      // Call down to kernel mode, using the user attachment channel.
      // Issue the T120 request to kernel mode.
      Status = IcaStackIoControl(pUserInfo->pDomain->hIcaStack,
              IOCTL_T120_REQUEST, &DUrq, sizeof(DUrq), &MCSErr, sizeof(MCSErr),
              &BytesReturned);
      if (!NT_SUCCESS(Status)) {
          ErrOutIca(pDomain->hIca, "DetachUserRequest(): T120 request failed");
          return MCS_USER_NOT_ATTACHED;
      }
      if (MCSErr != MCS_NO_ERROR)
          return MCSErr;
  
      // Remove the hUser from the User list.
      EnterCriticalSection(&g_csGlobalListLock);
      SListRemove(&g_UserList, (unsigned)hUser, &pDomain);
      LeaveCriticalSection(&g_csGlobalListLock);
      if (pDomain == NULL)
          return MCS_NO_SUCH_USER;
      
      // Clean up contents of pUserInfo then free.
      DestroyUserInfo(pUserInfo);
      Free(pUserInfo);
  
      return MCS_NO_ERROR;
  }
  #endif  // MCS_Future
  
  
  
  #if MCS_Future
  /*
   * Join a data channel. Once joined, an attachment can receive data sent on
   * that channel. Static channels are in range 1..1000 and can be joined by
   * any user. Dynamic channels are in range 1001..65535 and cannot be joined
   * unless they are convened by a user and the convenor allows this user
   * to be admitted, or the dynamic channel is a previously assigned channel
   * requested by a user attachment calling JoinRequest() with a channel ID
   * of 0.
   */
  
  MCSError APIENTRY MCSChannelJoinRequest(
          UserHandle    hUser,
          ChannelID     ChannelID,
          ChannelHandle *phChannel,
          BOOLEAN       *pbCompleted)
  {
      unsigned Err, BytesReturned;
      NTSTATUS Status;
      MCSChannel *pMCSChannel;
      UserInformation *pUserInfo;
      ChannelJoinReturnIoctl CJrt;
      ChannelJoinRequestIoctl CJrq;
  
      CheckInitialized("ChannelJoinReq()");
  
      pUserInfo = (UserInformation *)hUser;
      *pbCompleted = FALSE;
  
      // Alloc a new user-mode channel struct.
      pMCSChannel = Malloc(sizeof(MCSChannel));
      if (pMCSChannel == NULL) {
          ErrOutIca(pUserInfo->pDomain->hIca, "ChannelJoinReq(): "
                  "Could not alloc a user-mode channel");
          return MCS_ALLOCATION_FAILURE;
      }
      pMCSChannel->hChannelKernel = NULL;
      pMCSChannel->ChannelID = 0;
  
      // Add channel to user list of joined channels.
      if (!SListAppend(&pUserInfo->JoinedChannelList, (unsigned)pMCSChannel,
              pMCSChannel)) {
          ErrOutIca(pUserInfo->pDomain->hIca, "ChannelJoinReq(): "
                  "Could not add channel to user mode user list");
          CJrt.MCSErr = MCS_ALLOCATION_FAILURE;
          goto PostAlloc;
      }
  
      // Transfer params for kernel mode call.
      CJrq.Header.hUser = pUserInfo->hUserKernel;
      CJrq.Header.Type = MCS_CHANNEL_JOIN_REQUEST;
      CJrq.ChannelID = ChannelID;
  
      // Issue the T120 request to kernel mode
      Status = IcaStackIoControl(pUserInfo->pDomain->hIcaStack,
              IOCTL_T120_REQUEST, &CJrq, sizeof(CJrq), &CJrt, sizeof(CJrt),
              &BytesReturned);
      if (!NT_SUCCESS(Status)) {
          ErrOutIca1(pUserInfo->pDomain->hIca, "ChannelJoinReq(): "
                  "T120 request failed (%ld)", Status);
          CJrt.MCSErr = MCS_ALLOCATION_FAILURE;
          goto PostAddList;
      }
      if (CJrt.MCSErr != MCS_NO_ERROR)
          goto PostAddList;
  
      if (CJrt.bCompleted) {
          // Fill in the user-mode channel info.
          pMCSChannel->hChannelKernel = CJrt.hChannel;
          pMCSChannel->ChannelID = CJrt.ChannelID;
          *phChannel = pMCSChannel;
          *pbCompleted = TRUE;
      }
  
      return MCS_NO_ERROR;
  
  // Error handling.
  PostAddList:
      SListRemove(&pUserInfo->JoinedChannelList, (unsigned)pMCSChannel, NULL);
      
  PostAlloc:
      Free(pMCSChannel);
      return CJrt.MCSErr;
  }
  #endif  // MCS_Future
  
  
  
  #if MCS_Future
  /*
   * Leave a data channel previously joined. Once unjoined, the user attachment
   * does not receive data from that channel.
   */
  
  MCSError APIENTRY MCSChannelLeaveRequest(
          UserHandle    hUser,
          ChannelHandle hChannel)
  {
      unsigned BytesReturned;
      MCSError MCSErr;
      NTSTATUS Status;
      MCSChannel *pMCSChannel;
      UserInformation *pUserInfo;
      ChannelLeaveRequestIoctl CLrq;
  
      CheckInitialized("ChannelLeaveReq()");
  
      pUserInfo = (UserInformation *)hUser;
  
  #if DBG
      // Check that the indicated channel is actually present.
      if (!SListGetByKey(&pUserInfo->JoinedChannelList, (unsigned)hChannel,
              &pMCSChannel)) {
          ErrOutIca(pUserInfo->pDomain->hIca, "ChannelLeaveReq(): "
                  "Given hChannel not present!");
          return MCS_NO_SUCH_CHANNEL;
      }
  #endif
  
      pMCSChannel = (MCSChannel *)hChannel;
      
      // Transfer params.
      CLrq.Header.Type = MCS_CHANNEL_LEAVE_REQUEST;
      CLrq.Header.hUser = pUserInfo->hUserKernel;
      CLrq.hChannel = pMCSChannel->hChannelKernel;
  
      // Issue the T120 request to kernel mode
      Status = IcaStackIoControl(pUserInfo->pDomain->hIcaStack,
              IOCTL_T120_REQUEST, &CLrq, sizeof(CLrq), &MCSErr,
              sizeof(MCSErr), &BytesReturned);
      if (!NT_SUCCESS(Status)) {
          ErrOutIca1(pUserInfo->pDomain->hIca, "ChannelLeaveReq(): "
                  "T120 request failed (%ld)", Status);
          return MCS_ALLOCATION_FAILURE;
      }
      if (MCSErr != MCS_NO_ERROR)
          return MCSErr;
  
      // Remove the user-mode channel from the user list and destroy.
      SListRemove(&pUserInfo->JoinedChannelList, (unsigned)pMCSChannel, NULL);
      Free(pMCSChannel);
  
      return MCS_NO_ERROR;
  }
  #endif  // MCS_Future
  
  
  
  /*
   * Allocates a SendData buffer. This must be done by MCS to ensure high
   *   performance operation without memcpy()'s.
   */
  
  #if MCS_Future
  MCSError APIENTRY MCSGetBufferRequest(
          UserHandle hUser,
          unsigned Size,
          void **ppBuffer)
  {
      BYTE *pBuf;
  
      CheckInitialized("GetBufferReq()");
  
      // Leave sizeof(MCSSendDataRequestIoctl) in front of the block to bypass
      //   memcpy()'s during SendData.
      pBuf = Malloc(sizeof(SendDataRequestIoctl) + Size);
      if (pBuf == NULL) {
          ErrOut("GetBufferReq(): Malloc failed");
          return MCS_ALLOCATION_FAILURE;
      }
  
      *ppBuffer = pBuf + sizeof(SendDataRequestIoctl);
      return MCS_NO_ERROR;
  }
  #endif  // MCS_Future
  
  
  
  /*
   * Free a buffer allocated using GetBufferRequest(). This should only need to
   *   be done in the case where a buffer was allocated but never used.
   *   SendDataReq() automatically frees the buffer used before it returns.
   */
  
  #if MCS_Future
  MCSError APIENTRY MCSFreeBufferRequest(UserHandle hUser, void *pBuffer)
  {
      CheckInitialized("FreeBufReq()");
  
      ASSERT(pBuffer != NULL);
  
      // Reverse the process done in GetBufferReq() above.
      Free((BYTE *)pBuffer - sizeof(SendDataRequestIoctl));
  
      return MCS_NO_ERROR;
  }
  #endif  // MCS_Future
  
  
  
  #if MCS_Future
  /*
   * Sends data on a channel. The channel need not have been joined before
   * sending. The pData[] buffers must have been generated from successful
   * calls to MCSGetBufferRequest(). After this call the pData[] are invalid;
   * MCS will free them, and assign NULL to the contents in pData[].
   */
  
  MCSError APIENTRY MCSSendDataRequest(
          UserHandle      hUser,
          DataRequestType RequestType,
          ChannelHandle   hChannel,
          ChannelID       ChannelID,
          MCSPriority     Priority,
          Segmentation    Segmentation,
          BYTE            *pData,
          unsigned        DataLength)
  {
      unsigned BytesReturned;
      NTSTATUS Status;
      MCSError MCSErr;
      MCSChannel *pMCSChannel;
      UserInformation *pUserInfo;
      SendDataRequestIoctl *pSDrq;
  
      CheckInitialized("SendDataReq()");
      
      ASSERT(pData != NULL);
  
      pUserInfo = (UserInformation *)hUser;
  
  #if DBG
      // Check against maximum send size allowed.
      if (DataLength > pUserInfo->MaxSendSize) {
          ErrOutIca(pUserInfo->pDomain->hIca, "SendDataReq(): Send size "
                  "exceeds negotiated domain maximum");
          return MCS_SEND_SIZE_TOO_LARGE;
      }
  #endif
  
      // Inbound buffer was allocated by GetBufferReq() with
      //   sizeof(MCSSendDataRequestIoctl) bytes at the beginning.
      pSDrq = (SendDataRequestIoctl *)(pData - sizeof(SendDataRequestIoctl));
  
      if (hChannel == NULL) {
          // The user requested a send to a channel that it has not joined.
  
          ASSERT(ChannelID >= 1 && ChannelID <= 65535);
  
          // Forward the channel number to kernel mode for handling.
          pSDrq->hChannel = NULL;
          pSDrq->ChannelID = ChannelID;
      }
      else {
  
  #if DBG
          // Check that the indicated channel is actually present.
          if (!SListGetByKey(&pUserInfo->JoinedChannelList, (unsigned)hChannel,
                  &pMCSChannel)) {
              ErrOutIca(pUserInfo->pDomain->hIca, "SendDataReq(): "
                      "Given hChannel not joined by user!");
              return MCS_NO_SUCH_CHANNEL;
          }
  #endif
  
          pMCSChannel = (MCSChannel *)hChannel;
  
          pSDrq->hChannel = pMCSChannel->hChannelKernel;
          pSDrq->ChannelID = 0;
      }
  
      // Fill out data for sending to kernel mode.
      pSDrq->Header.Type = (RequestType == NORMAL_SEND_DATA ?
              MCS_SEND_DATA_REQUEST : MCS_UNIFORM_SEND_DATA_REQUEST);
      pSDrq->Header.hUser = pUserInfo->hUserKernel;
      pSDrq->RequestType = RequestType;
      pSDrq->Priority = Priority;
      pSDrq->Segmentation = Segmentation;
      pSDrq->DataLength = DataLength;
  
      // Issue the T120 request to kernel mode.
      Status = IcaStackIoControl(pUserInfo->pDomain->hIcaStack,
              IOCTL_T120_REQUEST, (BYTE *)pSDrq,
              sizeof(SendDataRequestIoctl) + DataLength,
              &MCSErr, sizeof(MCSErr), &BytesReturned);
      if (!NT_SUCCESS(Status)) {
          ErrOutIca1(pUserInfo->pDomain->hIca, "MCSSendDataReq(): "
                  "T120 request failed (%ld)", Status);
          return MCS_ALLOCATION_FAILURE;
      }
      if (MCSErr != MCS_NO_ERROR)
          return MCSErr;
  
      // Buffer sent to kernel mode is copied as necessary. We free
      //   the memory after we are done.
      MCSFreeBufferRequest(hUser, pData);
  
      return MCS_NO_ERROR;
  }
  #endif  // MCS_Future
  
  