using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using TradeIdeas.ServerConnection;
namespace TradeIdeas.TIQData
{
///
/// This is used by the delegate.
///
public struct PreviewArgs
{
///
/// True corresponds to a green tick mark in TI Pro. False corresponds to a red tick mark.
///
public bool goodMessage;
///
/// This is used internally for our testing and development. It is not used by TI Pro,
/// and should have no value to an API user.
///
public byte[] messageBody;
}
///
/// This is used by the delegate.
///
public struct ConnectionStatusCallbackArgs
{
///
/// This is a user friendly description of the current status. It will not be null.
///
public string message;
///
/// The TCP/IP connection is broken. This is usually associated with one or more yellow tickmarks.
///
public bool isServerDisconnect;
}
///
/// This provides a status message safe to show the user.
///
/// The object making the callback.
/// Details of the status.
public delegate void ConnectionStatusUpdate(ConnectionBase source, ConnectionStatusCallbackArgs args);
///
/// This provides status in a way that the toolbar can display graphically.
///
/// The object making the callback.
/// Details of this event.
public delegate void Preview(ConnectionBase source, PreviewArgs args);
///
/// This is what we export from to the main program. More
/// might be added over time, but I think it's best if we don't export everything.
/// Parts of only makes sense for the core classes, like .
///
public interface ConnectionBase
{
///
/// Instructions for creating a new connection. (I.e. TCP/IP address and port number.)
/// This can come from a config file or a user selection. It's best not to hard code
/// this, in case you ever need to talk with a test server.
///
IConnectionFactory ConnectionFactory { get; set; }
///
/// Notifies the listener to a variety of conditions. Like all events generated by
/// TIProData, this can come from *any* thread.
///
event ConnectionStatusUpdate ConnectionStatusUpdate;
///
/// Notifies the listener to message activity. Like all events generated by
/// TIProData, this can come from *any* thread.
///
event Preview Preview;
};
///
/// This class is responsible for creating and recreating the connection to the server.
/// TalkWithServer is used by several elements of the program, but this is the one part
/// responsible for creating and destroying TalkWithServer. This object also contains
/// the status of the connection, and a timer.
///
internal class ConnectionLifeCycle : ConnectionBase, TalkWithServer.IListener, IDisposable
{ // In the Delphi version of the program, this was tightly coupled with other actions,
// like login, ping, and requesting alert data. In this version we've seperated those
// functions into different classes. We use events to allow those other classes to
// hook into events in this object.
///
/// Create a new one. Typically there is only one per program, but that is not required.
/// One total, not just one at a time.
///
public ConnectionLifeCycle()
{
Thread thread = new Thread(new ThreadStart(DoTimerThread));
thread.IsBackground = true;
thread.Start();
}
// Can C# automatically pull the documentation from the Interface? It seems silly
// do duplicate it here.
public event ConnectionStatusUpdate ConnectionStatusUpdate;
public event Preview Preview;
///
/// Use this to create new connections.
///
private volatile IConnectionFactory _connectionFactory;
///
/// Use to lock. Replaces the lock(this)
///
private object _mutex = new object();
///
/// Use this to create new connections.
///
public IConnectionFactory ConnectionFactory
{
get { return _connectionFactory; }
set
{
if (_connectionFactory != value)
{
_connectionFactory = value;
SoftReset();
}
}
}
///
/// Full means that we are logged in. For the most part all background services
/// are on when we are in the Full status, and off when we are in any other state.
/// We try to keep the connection going, reconnecting as needed, when in this
/// state. Note that logged in can mean logged in as DEMO, so you don't really
/// need an account.
///
/// This is different than the Delphi version. The Delphi version tried to
/// automatically disconnect when you had no alert windows. That was a nice idea,
/// but made things more complicated. and it wasn't perfect because you might
//// be using other features, like symbol lists, even with no alerts. It is up
/// to the main program to tell this library to log in or out to make or break
/// the connection.
///
/// Limited exists for historical reasons. Some requests might come up even if
/// we are not logged in. Most of the background services, like ping, are not
/// active. So very likely the server will eventually break the connection
/// after a timeout. We don't try to reconnect. This mostly works with quick
/// requests, like downloading a small file.
///
/// None means that we are disconnected, or we will be soon. We go into this
/// state as soon as disconnect is requested. The disconnect is performed in
/// a different thread, based on the status.
///
public enum ConnectionStatus
{
Full,
Limited,
None
}
///
/// The status of the connection. In particular, should we automatically
/// restart if there is a problem.
///
public volatile ConnectionStatus Status = ConnectionStatus.None;
///
/// This is called whenever we create a new connection. We pass a copy of the TalkWithServer so the
/// callback won't have to call GetServerConnection(). That avoids the possility of some type of loop.
/// If one of the callbacks closed the connection, and the next callback tried to send a message,
/// that would cause the loop.
///
/// The object that sent this event.
/// The newly created server connection.
public delegate void ConnectionCallback(ConnectionLifeCycle source, TalkWithServer serverConnection);
///
/// Listen to new connection events. This is required by a number of other classes. Any type of
/// subscription (like alerts for an alert window) must be restarted each time we reconnect.
///
/// Like all callbacks from TIProData, this can come from any thread.
/// Note: The details of broken connections have changed. Originally a request could only fail
/// if the server broke the connection. All pending requests would fail at the same time. It
/// made some sense to wait for this message before retrying. (If you call SendManager to send a
/// message, SendManager will take care of waiting for you.) That is no longer true. Now that
/// we talk with the micro_proxy, it is possible that the connection to just one server failed,
/// so only some of the pending requests will fail. We only get this event if we reconnect to
/// the proxy. So in the past you could listen to either event (OnConnection or a failure in the
/// callback from the original request), whichever was more convenient. Now you almost always
/// should listen to the callback from the original request. This callback is only aimed at a
/// few specific low level classes, like LoginManager and SendManager.
///
/// Note: To help with the transition, in some cases we preserve the old behavior. The
/// micro_proxy is responsible for that. Sometimes it will break the entire connection even though
/// only one server went down. Newer code should only listen to the invidivual request callbacks.
/// But old code (the alerts, in particular) can still rely on the OnConnection callback.
///
/// Note: Each ConnectionLifeCycle object has it's own thread. This thread exists for the entire
/// lifetime of the object. This thread is the only place where we create new connections.
/// Immediately after creating the new connection we call this event. No one can do anything with
/// the new connection before this callback because this callback is the only way the outside
/// world can get a pointer to the new connection. This gaurentees that the initialization will
/// be complete before any other messages are sent. The OnTimer event fires in this same thread.
///
/// Note: The previous paragraph describes a new feature of the code. Originally any peice of
/// code in any thread could ask for the connection. If the connection was down, we'd create the
/// new connection in the caller's thread. That includes calling the callbacks associated with
/// this event in the same thread. That caused problems. It was hard to be certain that
/// the initialization was complete before sending other messages from other threads. (I chose
/// not to wrap the entire initialization in a mutex. As a general rule it's a bad idea to make
/// arbitrary callbacks from inside a mutex. That rules makes it easy to avoid deadlock.)
///
/// I'm assuming that the individual callbacks are made in the order in which people added them.
/// The internet suggests that this not as simple as it sounds.
/// http://stackoverflow.com/questions/1645478/order-of-event-handler-execution
///
public event ConnectionCallback OnConnection;
///
/// The current ServerConnection.
///
/// The server connection is only created in GetServerConnection. And that's the only method
/// which sets this field to a non-null value. That's protected by _mutex, to make sure we
/// don't create two of these at the same time.
///
/// We never set this back to null. We used to at one time, but that caused some complications.
/// GetServerConnection now checks if the server connection is old and needs to be recreated.
/// TalkWithServer already contains a field to track that, with appropriate thread safety. I
/// Think we used to set _serverConnection to null because that's how the old Delphi design
/// worked, but that design was not multi-threaded.
///
private TalkWithServer _serverConnection;
///
/// If the server connection does not exist or it is closed, create a new one. Otherwise,
/// do nothing. Call OnConnection if we created a new connection.
///
private void VerifyServerConnection()
{
bool created = false;
TalkWithServer serverConnection = null;
lock (_mutex)
{
if ((_serverConnection == null) || (_serverConnection.GetStatus() == TalkWithServer.Status.scsDisconnected))
{ // Create Server Connection
created = true;
// Interesting. Each instance of TalkWithServer only sends messages from the server to the rest of the
// program in one thread. The messages are kept in sequence that way. However, each TalkwithServer has
// it's own thread. And this class uses a different thread to create a new TalkWithServer. So the program
// can receive two diffrent messages from the server at the same time in different threads. This happens
// a lot when I'm stepping line by line in the debugger.
_serverConnection = new TalkWithServer(_connectionFactory, (TalkWithServer.IListener)this);
serverConnection = _serverConnection;
_serverConnection.Connect();
// Note: It is possible for the Connect() call to fail and for us to get a callback in this same thread,
// before the next line is executed. At one time we were setting _serverConnection to null in that
// callback. We never set it back to null any more. (It can be null, but it can never transition from
// not null back to null.
}
else
serverConnection = _serverConnection;
}
// As usual, do the callback outside of the lock to avoid the possibility of deadlock.
if (created)
{
ConnectionCallback callback = OnConnection;
if (callback != null)
callback(this, serverConnection);
}
}
///
/// Used for timer events.
///
///
public delegate void Callback(ConnectionLifeCycle source);
///
/// Break the connection immediately. Depending on the state we are in, we will
/// automatically reconnect soon.
///
/// This is similar to what we do when we see an unexpected network disconnect.
/// We try to reconnect in a why that has the least impact on the user.
///
public void SoftReset()
{ // Do not try to lock anything. TalkWithServer.Disconnect() will include
// arbitrary callbacks, and we don't want to hold a lock when those
// callbacks are being made. We don't worry about calling disconnect
// multiple times on the object. TalkWithServer takes care of this on
// it's own. And we don't worry about calling disconnect from the
// wrong thread or outside of a lock. Disconnect could happen from the
// server side anyway, so we have no real control over threads or locks.
// It is explicity safe to call various methods on a disconnected
// TalkWithServer object. TalkWithServer makes sure these calls won't
// cause an exception.
if (_serverConnection != null)
// Note that there is one difference between calling
// TalkWithServer.Disconnect() and when the server initiates the
// disconnect. In the latter case, a callback is send to us.
// When we initiate the disconnect we do not get a callback.
_serverConnection.Disconnect();
}
///
/// Set the connection status.
///
/// This is mostly a meeting place between the various things that can detect errors and
/// the GUI which displays the status.
///
/// A user friendly message. Must not be null.
/// Is this a problem?
public void SetStatus(string statusMessage, bool error = false)
{
ConnectionStatusCallbackArgs args;
args.message = statusMessage;
args.isServerDisconnect = error;
ConnectionStatusUpdate callback = ConnectionStatusUpdate;
if (null != callback)
try
{
callback(this, args);
}
catch
{
// keep working
}
}
///
/// A timer to help us restart the connection, as needed.
///
/// This timer is also exported to other core units. It can be a convenience so other units
/// don't have to create their own timer. But it's also controled. We update the connection
/// (if required) before calling the other units.
///
/// Normally the timer goes off approximately twice a second. However, a call to
/// WakeUpSoon() will cause the timer to go off almost immediately. SendManager calls
/// WakeUpSoon() frequently.
///
/// This always fires in the same thread as OnConnection.
///
public event Callback OnTimer;
private readonly AutoResetEvent _wakeUp = new AutoResetEvent(false);
///
/// Use a seperate thread to handle the restarts. This gives us some control, not being
/// called by a bunch of threads. And this makes the code simpler than responding to an
/// even immediately, because we know what state we are in. And this keeps us from bringing
/// the CPU to 100% and/or using up all the stack if the connection keeps failing.
///
private void DoTimerThread()
{
while (!_endThread)
{
if (Status == ConnectionStatus.None)
{ // Someone asked to break the current connection.
// I don't like this at all. It seems like the requests could get mixed up.
Status = ConnectionStatus.Limited;
SoftReset();
}
else if (Status == ConnectionStatus.Full)
{ // We want a constant connection.
TalkWithServer serverConnection = _serverConnection;
if ((serverConnection == null) || (_serverConnection.GetStatus() == TalkWithServer.Status.scsDisconnected))
// Create it the first time!
VerifyServerConnection();
}
Callback callback = OnTimer;
if (callback != null)
callback(this);
_wakeUp.WaitOne(501);
}
if (_serverConnection != null)
_serverConnection.Disconnect();
}
///
/// There is a timer that goes off about twice a second normally, but this makes it go off
/// much sooner. The timer is used internally to see if we need to create a new connection.
/// It also causes the OnTimer event to fire.
///
public void WakeUpSoon()
{
_wakeUp.Set();
}
///
/// Respond to a notification from the lower level objects.
/// Report the error message immediately.
/// Record the new status so our timer thread can take action later.
///
/// Something user friendly. This must not be null.
void TalkWithServer.IListener.OnDisconnected(String errorMessage)
{
if (Status == ConnectionStatus.None)
// Ignore this message. We initiated the disconnect, not the server.
// We already have a better status message.
return;
SetStatus(errorMessage, true); // Set isServerDisconnect to true
}
///
/// Currently this message is being ignored.
/// In TI Pro 2.x we reported more errrors.
/// This is mostly aimed at the HTTP connection.
///
/// Something user friendly. This must not be null.
void TalkWithServer.IListener.OnAutoRetry(String errorMessage)
{
}
///
/// We received a message, or a message was abandoned because the connection was broken.
///
/// The original message, or null on error.
void TalkWithServer.IListener.OnMessagePreview(TalkWithServer.PreviewArgs origArgs)
{
Preview callback = Preview;
if (null != Preview)
try
{
PreviewArgs args;
if (origArgs.body != null)
{
args.goodMessage = true;
args.messageBody = origArgs.body;
}
else
{
args.goodMessage = false;
args.messageBody = null;
}
Preview(this, args);
}
catch
{
// keep working
}
}
///
/// Request that the thread shut down. This only happens when we are done with this object.
///
private volatile bool _endThread = false;
///
/// Clean up.
///
~ConnectionLifeCycle()
{
Dispose();
}
///
/// Cleanup. Because this object owns it's own thread, it's not likely to get garbage collected
/// unless you call this method.
///
/// It is the caller's responsibility not to call GetServerConnection() after calling this.
/// Because of the threads and locks it would be difficult for this object to detect the
/// problem.
///
public void Dispose()
{
_endThread = true;
}
}
}