Subject: ClientSocket does not properly clean up in #Disconnect()
I was writing a client that needed to maintain a constant connection to the jabber server. To do this I detect when there is a disconnect event, or no current connection, etc. and reconnect. Before connecting I would call XmppClientConnection#Close followed by XmppClientConnection#SocketDisconnect. I discovered that this would not work. I first thought it was an error with my code so I began to only call XmppClientConnection#Close when the XmppState was Connected or SessionStarted. However, this resulted in the client sometimes unable to regain a connection.
I went back to my initial problem and found that calling XmppClientConnection#Close before the XmppClientConnection#Open would result in never reaching the XmppState.SessionStarted and it would stay stuck in the Connected state.
I traced the through what happens when you call the XmppClientConnection#Close method. It’s a call to the base method XmppConnection#Close. Which calls XmppConnection#Send. This results in calling XmppConnection#m_ClientSocket#Send. For an XmppClientConnection this is a ClientSocket object. This is where it gets interesting.
~ line 554 of ClientSocket.cs
On the send, if not set, m_PendingSend is set true then the send is attempted. If there is an exception, such as the socket isn’t connected, or any other error then ClientSocket#Disconnect() is called. So if you haven’t called ClientSocket#Connect yet (null exception) or if the socket is disconnect or otherwise bad (IOException) m_PendingSend will continue to be set true. Then when you subsequently call ClientSocket#Connect to reconnect it creates a new socket but m_PendingSend hasn’t been reset. So then when XmppClientConnection attempts to initialize the stream no data is ever sent to the server.
A fix is therefore needed in the ClientSocket#Disconnect function.
The new code is right after the base.Disconnect() and before the first if statement. This resets m_PendingSend and clears the send queue readying the class for the next Connect call.
TEST PROGRAM:
Program.cs
JabberClient.cs
If you run this code with the current version of the agsXMPP library it will never connect to the jabber server. However, if you add in the fix to the ClientSocket#Disconnect function then it will work as expected.
This was a bit of an exasperating bug to figure out but the fix is quite simple and removes all problems I’ve observed.
I went back to my initial problem and found that calling XmppClientConnection#Close before the XmppClientConnection#Open would result in never reaching the XmppState.SessionStarted and it would stay stuck in the Connected state.
I traced the through what happens when you call the XmppClientConnection#Close method. It’s a call to the base method XmppConnection#Close. Which calls XmppConnection#Send. This results in calling XmppConnection#m_ClientSocket#Send. For an XmppClientConnection this is a ClientSocket object. This is where it gets interesting.
~ line 554 of ClientSocket.cs
if (m_PendingSend)
{
m_SendQueue.Enqueue(bData);
}
else
{
m_PendingSend = true;
try
{
m_NetworkStream.BeginWrite(bData, 0, bData.Length, new AsyncCallback(EndSend), null);
}
catch(Exception ex)
{
Disconnect();
}
}
{
m_SendQueue.Enqueue(bData);
}
else
{
m_PendingSend = true;
try
{
m_NetworkStream.BeginWrite(bData, 0, bData.Length, new AsyncCallback(EndSend), null);
}
catch(Exception ex)
{
Disconnect();
}
}
On the send, if not set, m_PendingSend is set true then the send is attempted. If there is an exception, such as the socket isn’t connected, or any other error then ClientSocket#Disconnect() is called. So if you haven’t called ClientSocket#Connect yet (null exception) or if the socket is disconnect or otherwise bad (IOException) m_PendingSend will continue to be set true. Then when you subsequently call ClientSocket#Connect to reconnect it creates a new socket but m_PendingSend hasn’t been reset. So then when XmppClientConnection attempts to initialize the stream no data is ever sent to the server.
A fix is therefore needed in the ClientSocket#Disconnect function.
public override void Disconnect()
{
base.Disconnect();
lock (this)
{
m_PendingSend = false;
m_SendQueue.Clear();
}
// return right away if have not created socket
if (_socket == null)
return;
try
{
// first, shutdown the socket
_socket.Shutdown(SocketShutdown.Both);
}
catch {}
try
{
// next, close the socket which terminates any pending
// async operations
_socket.Close();
}
catch {}
FireOnDisconnect();
}
{
base.Disconnect();
lock (this)
{
m_PendingSend = false;
m_SendQueue.Clear();
}
// return right away if have not created socket
if (_socket == null)
return;
try
{
// first, shutdown the socket
_socket.Shutdown(SocketShutdown.Both);
}
catch {}
try
{
// next, close the socket which terminates any pending
// async operations
_socket.Close();
}
catch {}
FireOnDisconnect();
}
The new code is right after the base.Disconnect() and before the first if statement. This resets m_PendingSend and clears the send queue readying the class for the next Connect call.
TEST PROGRAM:
Program.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
namespace ReconnectProblem
{
class Program
{
private static StreamWriter str;
static void Main(string[] args)
{
str = new StreamWriter(new FileStream("log_" + System.Diagnostics.Process.GetCurrentProcess().Id + ".txt",
FileMode.OpenOrCreate, FileAccess.Write));
Program.log("Type 'exit' to exit, 'con' to connect, and 'dis' to disconnect");
JabberClient c = new JabberClient();
string line;
while(true)
{
line = Console.ReadLine();
switch(line)
{
case "exit":
Program.log("Exiting...");
return;
break;
case "con":
c.Connect();
break;
case "dis":
c.Disconnect();
break;
default:
break;
}
}
}
public static void log(string line)
{
Console.WriteLine(line);
str.WriteLine(line);
str.Flush();
}
}
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
namespace ReconnectProblem
{
class Program
{
private static StreamWriter str;
static void Main(string[] args)
{
str = new StreamWriter(new FileStream("log_" + System.Diagnostics.Process.GetCurrentProcess().Id + ".txt",
FileMode.OpenOrCreate, FileAccess.Write));
Program.log("Type 'exit' to exit, 'con' to connect, and 'dis' to disconnect");
JabberClient c = new JabberClient();
string line;
while(true)
{
line = Console.ReadLine();
switch(line)
{
case "exit":
Program.log("Exiting...");
return;
break;
case "con":
c.Connect();
break;
case "dis":
c.Disconnect();
break;
default:
break;
}
}
}
public static void log(string line)
{
Console.WriteLine(line);
str.WriteLine(line);
str.Flush();
}
}
JabberClient.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using agsXMPP;
namespace ReconnectProblem
{
internal class JabberClient
{
protected agsXMPP.XmppClientConnection clientConnection = null;
protected int connecting = 0;
public JabberClient()
{
clientConnection = new agsXMPP.XmppClientConnection();
clientConnection.OnLogin += clientConnection_OnLogin;
clientConnection.OnClose += clientConnection_OnClose;
clientConnection.OnError += clientConnection_OnError;
clientConnection.OnMessage += clientConnection_OnMessage;
clientConnection.OnRosterEnd += clientConnection_OnRosterEnd;
clientConnection.OnReadXml += clientConnection_OnReadXml;
clientConnection.OnWriteXml += clientConnection_OnWriteXml;
clientConnection.ClientSocket.OnConnect += ClientSocket_OnConnect;
clientConnection.OnSocketError += clientConnection_OnSocketError;
clientConnection.OnAuthError += clientConnection_OnAuthError;
clientConnection.OnXmppError += clientConnection_OnXmppError;
clientConnection.OnXmppConnectionStateChanged += clientConnection_OnXmppConnectionStateChanged;
}
void clientConnection_OnXmppError(object sender, agsXMPP.Xml.Dom.Element e)
{
Program.log("Jabber: OnXmppError: " + e);
}
void clientConnection_OnXmppConnectionStateChanged(object sender, XmppConnectionState state)
{
Program.log("Jabber: OnXmppConnectionStateChanged: " + state);
}
public bool IsConnected
{
get
{
Program.log("Jabber: state: " + this.clientConnection.XmppConnectionState);
if (clientConnection.XmppConnectionState == agsXMPP.XmppConnectionState.SessionStarted) {
return true;
}
else {
return false;
}
}
}
public void Connect()
{
if(!IsConnected && Interlocked.CompareExchange(ref connecting, 1, 0) == 0) {
Program.log("Jabber Connect(): Connecting");
Disconnect();
clientConnection.Server = "grasshopper";
clientConnection.Username = "test";
clientConnection.Password = "test";
clientConnection.Port = 5222;
clientConnection.Resource = "test";
clientConnection.ConnectServer = "192.168.1.11";
clientConnection.Open();
Interlocked.Exchange(ref connecting, 0); // reset
}
else {
Program.log("Jabber Connect(): Already connected or already connecting");
}
}
public void Disconnect()
{
Program.log("Jabber: Disconnect()");
try
{
Program.log("Jabber: Disconnect(): Close()");
clientConnection.Close();
}
catch (Exception e)
{
Program.log("Jabber: Close(): Exception: " + e.Message);
}
try
{
Program.log("Jabber: Disconnect(): SocketDisconnect()");
clientConnection.SocketDisconnect();
} catch(Exception e)
{
Program.log("Jabber: ClientSocket.Disconnect(): Exception:" + e.Message);
}
}
public void Send(agsXMPP.Xml.Dom.Element e)
{
if (IsConnected) {
clientConnection.Send(e);
}
else {
Program.log("Jabber: not connected, current state: " + this.clientConnection.XmppConnectionState);
}
}
void ClientSocket_OnConnect(object sender)
{
Program.log("Jabber: clientSocket: OnConnect");
}
private void clientConnection_OnReadXml(object sender, string xml)
{
Program.log("Jabber: OnReadXml: " + xml);
}
private void clientConnection_OnWriteXml(object sender, string xml)
{
Program.log("Jabber: OnWriteXml: " + xml);
}
private void clientConnection_OnMessage(object sender, agsXMPP.protocol.client.Message msg)
{
Program.log("Jabber: OnMessage: " + msg.ToString());
}
private void clientConnection_OnError(object sender, Exception ex)
{
Program.log("Jabber: OnError: " + ex.Message);
}
private void clientConnection_OnClose(object sender)
{
Program.log("Jabber: OnClosed");
}
private void clientConnection_OnLogin(object sender)
{
Program.log("Jabber: Logged in");
}
private void clientConnection_OnRosterEnd(object sender)
{
clientConnection.Show = agsXMPP.protocol.client.ShowType.NONE;
clientConnection.Status = "Online";
clientConnection.Priority = 0;
clientConnection.SendMyPresence();
}
void clientConnection_OnAuthError(object sender, agsXMPP.Xml.Dom.Element e)
{
Program.log("Jabber: OnAuthError: " + e);
this.Disconnect();
}
void clientConnection_OnSocketError(object sender, Exception ex)
{
Program.log("Jabber: OnSocketError: " + ex.Message + ": " + ex.GetBaseException().Message);
}
}
}
using System.Collections.Generic;
using System.Text;
using System.Threading;
using agsXMPP;
namespace ReconnectProblem
{
internal class JabberClient
{
protected agsXMPP.XmppClientConnection clientConnection = null;
protected int connecting = 0;
public JabberClient()
{
clientConnection = new agsXMPP.XmppClientConnection();
clientConnection.OnLogin += clientConnection_OnLogin;
clientConnection.OnClose += clientConnection_OnClose;
clientConnection.OnError += clientConnection_OnError;
clientConnection.OnMessage += clientConnection_OnMessage;
clientConnection.OnRosterEnd += clientConnection_OnRosterEnd;
clientConnection.OnReadXml += clientConnection_OnReadXml;
clientConnection.OnWriteXml += clientConnection_OnWriteXml;
clientConnection.ClientSocket.OnConnect += ClientSocket_OnConnect;
clientConnection.OnSocketError += clientConnection_OnSocketError;
clientConnection.OnAuthError += clientConnection_OnAuthError;
clientConnection.OnXmppError += clientConnection_OnXmppError;
clientConnection.OnXmppConnectionStateChanged += clientConnection_OnXmppConnectionStateChanged;
}
void clientConnection_OnXmppError(object sender, agsXMPP.Xml.Dom.Element e)
{
Program.log("Jabber: OnXmppError: " + e);
}
void clientConnection_OnXmppConnectionStateChanged(object sender, XmppConnectionState state)
{
Program.log("Jabber: OnXmppConnectionStateChanged: " + state);
}
public bool IsConnected
{
get
{
Program.log("Jabber: state: " + this.clientConnection.XmppConnectionState);
if (clientConnection.XmppConnectionState == agsXMPP.XmppConnectionState.SessionStarted) {
return true;
}
else {
return false;
}
}
}
public void Connect()
{
if(!IsConnected && Interlocked.CompareExchange(ref connecting, 1, 0) == 0) {
Program.log("Jabber Connect(): Connecting");
Disconnect();
clientConnection.Server = "grasshopper";
clientConnection.Username = "test";
clientConnection.Password = "test";
clientConnection.Port = 5222;
clientConnection.Resource = "test";
clientConnection.ConnectServer = "192.168.1.11";
clientConnection.Open();
Interlocked.Exchange(ref connecting, 0); // reset
}
else {
Program.log("Jabber Connect(): Already connected or already connecting");
}
}
public void Disconnect()
{
Program.log("Jabber: Disconnect()");
try
{
Program.log("Jabber: Disconnect(): Close()");
clientConnection.Close();
}
catch (Exception e)
{
Program.log("Jabber: Close(): Exception: " + e.Message);
}
try
{
Program.log("Jabber: Disconnect(): SocketDisconnect()");
clientConnection.SocketDisconnect();
} catch(Exception e)
{
Program.log("Jabber: ClientSocket.Disconnect(): Exception:" + e.Message);
}
}
public void Send(agsXMPP.Xml.Dom.Element e)
{
if (IsConnected) {
clientConnection.Send(e);
}
else {
Program.log("Jabber: not connected, current state: " + this.clientConnection.XmppConnectionState);
}
}
void ClientSocket_OnConnect(object sender)
{
Program.log("Jabber: clientSocket: OnConnect");
}
private void clientConnection_OnReadXml(object sender, string xml)
{
Program.log("Jabber: OnReadXml: " + xml);
}
private void clientConnection_OnWriteXml(object sender, string xml)
{
Program.log("Jabber: OnWriteXml: " + xml);
}
private void clientConnection_OnMessage(object sender, agsXMPP.protocol.client.Message msg)
{
Program.log("Jabber: OnMessage: " + msg.ToString());
}
private void clientConnection_OnError(object sender, Exception ex)
{
Program.log("Jabber: OnError: " + ex.Message);
}
private void clientConnection_OnClose(object sender)
{
Program.log("Jabber: OnClosed");
}
private void clientConnection_OnLogin(object sender)
{
Program.log("Jabber: Logged in");
}
private void clientConnection_OnRosterEnd(object sender)
{
clientConnection.Show = agsXMPP.protocol.client.ShowType.NONE;
clientConnection.Status = "Online";
clientConnection.Priority = 0;
clientConnection.SendMyPresence();
}
void clientConnection_OnAuthError(object sender, agsXMPP.Xml.Dom.Element e)
{
Program.log("Jabber: OnAuthError: " + e);
this.Disconnect();
}
void clientConnection_OnSocketError(object sender, Exception ex)
{
Program.log("Jabber: OnSocketError: " + ex.Message + ": " + ex.GetBaseException().Message);
}
}
}
If you run this code with the current version of the agsXMPP library it will never connect to the jabber server. However, if you add in the fix to the ClientSocket#Disconnect function then it will work as expected.
This was a bit of an exasperating bug to figure out but the fix is quite simple and removes all problems I’ve observed.