views:

712

answers:

3

My goal is to have this program send a logout command when the user is logging off or shutting down their pc.

The program is connected to a server application via a tcp socket using a Winsock object. Calling singleSock.SendData "quit" & vbCrLf is simply a way of logging out. I am going to start capturing data with Wireshark, but I'd like to know if I'm trying to do something fundamentally wrong.

Oddly enough, if I set Cancel to True, and allow a timer I have running to do the logout command, then call another unload, it works, however in testing this configuration (different code), this prevents the user from logging out the first time. They have to initiate a logout, it doesn't do anything, then they logout again and my program is gone at that point. Also oddly enough, in Vista the logout goes through after briefly displaying a screen saying my program was preventing the logout. Most of my deployment is on XP, which has the two logouts problem.

 Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)

   If UnloadMode = vbFormControlMenu Then
      Me.WindowState = vbMinimized
      Cancel = True
   Else

      If SHUTDOWN_FLAG = True Then
        Cancel = False
      Else
        Cancel = True
        SHUTDOWN_FLAG = True
      End If
      tmrSocket.Enabled = False
      SHUTDOWN_FLAG = True
      Sleep (1000)
      singleSock.SendData "quit" & vbCrLf

      Call pUnSubClass
      'If singleSock.state <> sckConnected Then
      '   singleSock.Close
      '   tmrSocket.Enabled = False
      '   LogThis "tmrSocket turned off"
      'End If
      DoEvents
   End If

End Sub
+1  A: 

You're not waiting for the Winsock control to actually send the "quit" message. The SendData method is asynchronous: it can return before the data has actually been sent across the network. The data is buffered locally on your machine and sent at some later time by the network driver.

In your case, you are trying to send the "quit" message and then closing the socket almost immediately afterwards. Because SendData is asynchronous, the call might return before the "quit" message has actually been sent to the server, and therefore the code might close the socket before it has a chance to send the message.

It works when you cancel the unloading of the form first and let the timer send the "quit" message because you're giving the socket enough extra time to send the message to the server before the socket is closed. However, I wouldn't count on this always working; it's a coincidence that the extra steps gave the socket enough time to send the message, and it's not guaranteed to always work out that way.

You can fix the problem by waiting for the socket to raise a SendCompleted event after you send the "quit" message and before you close the socket. Below is a basic example. Note that the QueryUnload code is much simpler.

Private m_bSendCompleted As Boolean
Private m_bSocketError As Boolean

Private Sub singleSock_Error(ByVal Number As Integer, Description As String, ByVal Scode As Long, ByVal Source As String, ByVal HelpFile As String, ByVal HelpContext As Long, CancelDisplay As Boolean)
   'Set error flag so we know if a SendData call failed because of an error'
   'A more robust event handler could also store the error information so that'
   'it can be properly logged elsewhere'
   m_bSocketError = True
End Sub

Private Sub singleSock_SendCompleted()
   'Set send completed flag so we know when all our data has been sent to the server'
   m_bSendCompleted = True
End Sub

'Helper routine. Use this to send data to the server'
'when you need to make sure that the client sends all the data.'
'It will wait until all the data is sent, or until an error'
'occurs (timeout, connection reset, etc.).'
Private Sub SendMessageAndWait(ByVal sMessage As String)

   m_bSendCompleted = False
   singleSock.SendData sMessage

   singleSock.SendData sMessage

   Do Until m_bSendCompleted or m_bSocketError
      DoEvents
   Loop

   If m_bSocketError Then
      Err.Raise vbObjectError+1024,,"Socket error. Message may not have been sent."
   End If

End Sub

Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)

   'This is (almost) all the code needed to properly send the quit message'
   'and ensure that it is sent before the socket is closed. The only thing'
   'missing is some error-handling (because SendMessageAndWait could raise an error).'

   If UnloadMode = vbFormControlMenu Then
      Me.WindowState = vbMinimized
      Cancel = True
   Else
      SendMessageAndWait "quit" & vbCrLf
      singleSock.Close
   End If

End Sub

You can make the code cleaner by putting the logic to send a message and wait for it to be sent in a separate class. This keeps the private variables and the event handlers in one place, instead of having them litter your main code. It also makes it easier to re-use the code when you have multiple sockets. I called the class SynchronousMessageSender for lack of a better name. This example also has more complete error handling:

SynchronousMessageSender.cls

Private WithEvents m_Socket As Winsock
Private m_bAttached As Boolean

Private m_bSendCompleted As Boolean
Private m_bSocketError As Boolean

Private Type SocketError
    Number As Integer
    Description As String
    Source As String
    HelpFile As String
    HelpContext As Long
End Type

Private m_LastSocketError As SocketError

'Call this method first to attach the SynchronousMessageSender to a socket'
Public Sub AttachSocket(ByVal socket As Winsock)

    If m_bAttached Then
        Err.Raise 5,,"A socket is already associated with this SynchronousMessageSender instance."
    End If

    If socket Is Nothing Then
        Err.Raise 5,,"Argument error. 'socket' cannot be Nothing."
    End If

    Set m_Socket = socket

End Sub

Private Sub socket_SendCompleted()
    m_bSendCompleted = True
End Sub

Private Sub socket_Error(ByVal Number As Integer, Description As String, ByVal Scode As Long, ByVal Source As String, ByVal HelpFile As String, ByVal HelpContext As Long, CancelDisplay As Boolean)

    m_bSocketError = True

    'Store error information for later use'
    'Another option would be to create an Error event for this class'
    'and re-raise it here.'

    With m_lastSocketError
        .Number = Number
        .Description = Description
        .Source = Source
        .HelpFile = HelpFile
        .HelpContext = HelpContext
    End With

End Sub

'Sends the text in sMessage and does not return'
'until the data is sent or a socket error occurs.'
'If a socket error occurs, this routine will re-raise'
'the error back to the caller.'

Public Sub SendMessage(ByVal sMessage As String)

    If Not m_bAttached Then
        Err.Raise 5,,"No socket is associated with this SynchronousMessageSender. Call Attach method first."
    End If

    m_bSendCompleted = False
    m_bSocketError = False

    m_socket.SendData sMessage & vbCrLf

    'Wait until the message is sent or an error occurs'
    Do Until m_bSendCompleted Or m_bSocketError
        DoEvents
    Loop

    If m_bSocketError Then
        RaiseLastSocketError
    End If

End Sub

Private Sub RaiseLastSocketError()

    Err.Raise m_lastSocketError.Number, _
              m_lastSocketError.Source, _
              m_lastSocketError.Description, _
              m_lastSocketError.HelpFile, _
              m_lastSocketError.HelpContext

End Sub

Example Use

Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)

    Dim sender As New SynchronousMessageSender

   'Ignore errors since the application is closing...'
   On Error Resume Next

   If UnloadMode = vbFormControlMenu Then
      Me.WindowState = vbMinimized
      Cancel = True
   Else

      Set sender = New SynchronousMessageSender
      sender.AttachSocket singleSock
      sender.SendMessage "quit"
      singleSock.Close

   End If

End Sub

By using a separate class, now all the necessary code can be placed in the Form_QueryUnload, which keeps things tidier.

Mike Spross
+1  A: 

Wouldn't is be easier to just go without the QUIT command. In your server code just assume that the closing of a socket does the same thing as receiving a quit.

I addition, one thing you want to watch out for is abrupt shut downs of the client software. For example, a machine that losses power or network connection or a machine that goes into sleep or hibernate mode.

In those cases you should be periodically checking the connection for all of the clients from the server and closing any connections that do not respond to some kind of ping command.

Keith Maurino
A: 

Why is the sub named Private Sub singleSock_SendCompleted() when the actual event is named _SendComplete() ?