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; } } }