using System; using System.Collections.Generic; using System.Text; using System.Xml; using System.Diagnostics; using TradeIdeas.ServerConnection; using TradeIdeas.XML; using TradeIdeas.MiscSupport; namespace TradeIdeas.TIProData { /// /// Used when we have received new historical alerts. /// /// One or more alerts. The most recent one is on top. A user could iterate through /// this list in the normal order. There is no gaurentee about how many groups the alerts will come in. /// The history request that generated this data. /// Compare that to the currently outstanding request to make sure you are not /// receiving alerts from a request you already canceled. public delegate void HistoryData(List alerts, History sender); /// /// Status has changed. Read the status directly from the History object. /// /// The history request that generated this event. /// Compare that to the currently outstanding request to make sure you are not /// receiving events from a request you already canceled. public delegate void HistoryStatus(History sender); /// /// NotSent - The request has not been sent to the server. /// Working - The server is handling the request. /// MoreAvailable - The server stopped sending data because it sent all we asked for. /// Done - There is no more data or the client terminated the request. /// public enum HistoryDisposition { NotSent, Working, MoreAvailable, Done } /// /// Use this to specify the data you want to see. /// public struct HistoryRequest { /// /// This is a standard collaborate string. Do not include the leading "http://www.trade-ideas.com/View.php?" part. /// public string Config; /// /// The most recent alerts you want to see. Set this to null to disable this filter and see /// the most recent alerts available. /// public DateTime? StartTime; /// /// The oldest alerts you want to see. Set this to null to disable this filter and go back /// as far as the server will let you. /// public DateTime? EndTime; /// /// This is the number of alerts you want to see. /// /// You might get fewer. Maybe not enough alerts match your criteria. Or you run into some other /// limit. If you ask for 2 billion, your not going to get 2 billion. /// /// You might actually get more. For simplicity we don't make a guarantee. But you probably won't. /// /// This is the maximum you can get for each time you call start(). You can always ask for more. /// /// Set this to null to get a default value. /// public int? MaxCount; /// /// If you set MaxCount to null, that's the same as setting it to this value. /// public const int DEFAULT_COUNT = 1000; } /// /// Use this to interact with a history request. /// Use to create one of these. /// public interface History { /// /// New data is available. /// /// Like all callbacks from TIProData this can come from any thread. /// event HistoryData HistoryData; /// /// The status of this request has changed. /// /// Like all callbacks from TIProData this can come from any thread. This /// includes some possibly reentrant calls. If you cancel a history request, /// you are likely to see this callback before returns. /// event HistoryStatus HistoryStatus; /// /// There are strict rules about how history can change states. You can't reuse /// a history object. You might ask for more history, if you are in the appropriate /// state. /// HistoryDisposition HistoryDisposition { get; } /// /// Note: It's possible that we will go into this state on our own at any time. /// (More precisely, we can go from Working to MoreAvailable at any time.) /// Watch the status callback if you need to know that. However, we won't exit this /// state unless someone calls Start() or Stop(). /// /// True if it is safe and legal to call . bool CanStart(); /// /// Call start to start receiving the data. Nothing will be sent to the server until /// you do this. This gives you time to register for the callbacks. Also, this gives /// you time to store a copy of this object so when you get a response you are sure /// it is from the current object. If this object is in the MoreAvailable state, this /// will continue the last request from where we left off. This is like the user /// selecting "More" from the GUI. /// void Start(); /// /// Call this to stop the request early. This will save resources on the server and /// client side. /// /// It is always safe to call this. It doesn't make sense to ask for the current state /// because the state could change right after we ask. /// void Stop(); /// /// This gets updated with a connonical form, or "short form," by the server, just like /// the realtime stuff. /// string Config { get; } /// /// WindowName is null before the first update. "" means that (we know that) the user /// requested "". /// string WindowName { get; } /// /// This might be null before we hear from the server. /// IList Columns { get; } } /// /// The main program / API user should use this to create a new request. /// /// This class does a lot of stuff internally. All history requests share some resources. However, /// that is an implemention detail which is hidden. /// public class HistoryManager { private readonly object _mutex = new object(); private readonly Dictionary _windows = new Dictionary(); private class HistoryImpl : History { private static Int64 _lastId = 0; private readonly string _id; private HistoryManager _manager; public String Config { get; private set; } public String WindowName { get; private set; } public IList Columns { get; private set; } private int _maxCount; private string _startId = ""; private DateTime? _startTime; private DateTime? _endTime; private readonly int _perRequestCount; private const int MAX_PER_REQUEST_COUNT = 1000; public HistoryImpl(HistoryRequest request, HistoryManager manager) { lock (typeof(HistoryImpl)) { _lastId++; _id = "HA" + _lastId.ToString(); } Config = request.Config; _startTime = request.StartTime; _endTime = request.EndTime; _manager = manager; _perRequestCount = request.MaxCount??HistoryRequest.DEFAULT_COUNT; if (_perRequestCount > MAX_PER_REQUEST_COUNT) _perRequestCount = MAX_PER_REQUEST_COUNT; else if (_perRequestCount < 1) _perRequestCount = 1; } public void SendRequestNow() { // Assume we are already in the mutex. List message = new List(); message.Add("command"); message.Add("set_config_info"); message.Add("window_id"); message.Add(_id); message.Add("long_history"); message.Add("1"); message.Add("long_form"); message.Add(Config); message.Add("custom_columns"); // The server needs this to know that we are a newer client. message.Add(1); // Older clients get the built in columns. message.Add("non_filter_columns"); message.Add(1); if ("" != _startId) { message.Add("long_history_start_id"); message.Add(_startId); } else if (null != _startTime) { message.Add("long_history_start_time"); // TalkWithServer will convert this to time_t. message.Add(_startTime); } if (null != _endTime) { message.Add("long_history_end_time"); message.Add(_endTime); } message.Add("long_history_max_count"); message.Add(_maxCount); _manager.OpenChannel(); _manager._serverConnection.SendMessage( TalkWithServer.CreateMessage(message.ToArray()), Response, clientId: _manager._serverConnection); } private void Response(byte[] body, object session) { lock (_manager._mutex) { if (session != _manager._serverConnection) // This request came from an old connection. It's possible that we've // already restarted on the new session. if (HistoryDisposition != HistoryDisposition.Working) // The request was aborted. return; XmlNode wholeMessage = XmlHelper.Get(body); if (null == wholeMessage) // Let the HistoryManager restart the request at the right time. return; XmlNode status = wholeMessage.Node(0).Node("STATUS"); string shortForm = status.Property("SHORT_FORM", null); if (null != shortForm) Config = shortForm; WindowName = status.Property("WINDOW_NAME"); List columns = new List(); // Previously we would also check ALERT_COLUMNS here which included the // hard coded columns which are always sent, Symbol, Description, etc... foreach (XmlNode columnXml in status.Node("COLUMNS").Enum()) // add parameter for form type - RVH20210602 //columns.Add(new ColumnInfo(columnXml)); columns.Add(new ColumnInfo(columnXml, "ALERT_WINDOW")); Columns = columns.AsReadOnly(); } } public void Start() { lock (_manager._mutex) { Debug.Assert(CanStart()); HistoryDisposition = HistoryDisposition.Working; _manager._windows.Add(_id, this); _maxCount = _perRequestCount; if (null != _manager._serverConnection) SendRequestNow(); } SendStateChanged(); } public void Stop() { bool wasWorking; lock (_manager._mutex) { wasWorking = HistoryDisposition == HistoryDisposition.Working; HistoryDisposition = HistoryDisposition.Done; if (wasWorking) _manager._windows.Remove(_id); } if (wasWorking) { // Tell server to stop TalkWithServer serverConnection = _manager._serverConnection; if (null != serverConnection) // Note that this is exactly the same message that we send to stop // a realtime request. That's one of the reasons why realtime and // historical requests can't share an id. serverConnection.SendMessage( TalkWithServer.CreateMessage( "command", "remove_config_info", "window_id", _id)); // For consistency, notify the client. He's probably the one who requested // this (so it's probably a reentrant callback) and there isn't much he // can do about this. But maybe he grays out a menu item or something. SendStateChanged(); } } private void SendStateChanged() { HistoryStatus callback = HistoryStatus; if (null != HistoryStatus) try { HistoryStatus(this); } catch { } } private static RowData DecodeAlert(XmlNode asXml) { RowData result = new RowData(); foreach (XmlAttribute attribute in asXml.Attributes) result.Data[attribute.LocalName] = attribute.Value; return result; } private static List DecodeAlerts(XmlNode asXml) { List result = new List(); foreach (XmlNode alertNode in asXml.Enum()) result.Add(DecodeAlert(alertNode)); return result; } private XmlNode _unknownMessageTypeDebug; public void ProcessServerMessage(XmlNode message) { switch (message.Property("TYPE")) { case "alerts": { _startId = message.Property("RESTART"); HistoryData callback = HistoryData; if (null != callback) try { callback(DecodeAlerts(message), this); } catch { // Keep working even if the callback fails. } } break; case "finished": { bool stateChanged; lock (_manager._mutex) { HistoryDisposition newDisposition = (_startId=="")?HistoryDisposition.Done:HistoryDisposition.MoreAvailable; stateChanged = HistoryDisposition != newDisposition; HistoryDisposition = newDisposition; _manager._windows.Remove(_id); } if (stateChanged) SendStateChanged(); break; } default: // This is reserved. Today if we see it it's probably a bug. But to // allow for expansion we should ignore it. _unknownMessageTypeDebug = message; break; } } public HistoryDisposition HistoryDisposition { get; private set; } public bool CanStart() { return (HistoryDisposition == HistoryDisposition.NotSent) || (HistoryDisposition == HistoryDisposition.MoreAvailable); } public event HistoryData HistoryData; public event HistoryStatus HistoryStatus; } /// /// Create a new history request. /// /// What you want to see. /// An object that controls the request. public History GetHistory(HistoryRequest request) { return new HistoryImpl(request, this); } private TalkWithServer _serverConnection; internal HistoryManager(ConnectionLifeCycle connectionLifeCycle) { connectionLifeCycle.OnConnection += new ConnectionLifeCycle.ConnectionCallback(_connectionLifeCycle_OnConnection); } void _connectionLifeCycle_OnConnection(ConnectionLifeCycle sender, TalkWithServer serverConnection) { lock (_mutex) { _channelIsOpen = false; _serverConnection = serverConnection; foreach (KeyValuePair kvp in _windows) kvp.Value.SendRequestNow(); } } private bool _channelIsOpen; private void OpenChannel() { // Assume we are already in the mutex. if (_channelIsOpen) return; _channelIsOpen = true; _serverConnection.SendMessage( TalkWithServer.CreateMessage("command", "history_listen"), Response, streaming: true); } int _deletedWindowDataCount = 0; private void Response(byte[] body, object unused) { XmlNode wholeMessage = XmlHelper.Get(body); if (null == wholeMessage) // The onConnection message will automatically clean up. return; foreach (XmlNode windowData in wholeMessage.Node(0).Enum()) { HistoryImpl window = null; lock (_mutex) { string id = windowData.Property("WINDOW"); if (_windows.ContainsKey(id)) window = _windows[id]; } if (null == window) // This shouldn't happen very often. But sometimes it's unavoidable. // We could have sent a cancel message and the server was sending us // data, and the two messages crossed. It's tempting to send another // cancel message here, just to be safe. But that should not be // necessary. This is less important than in the realtime code becase // history responses are finite. _deletedWindowDataCount++; else // parse the data and sent it out. window.ProcessServerMessage(windowData); } } } }