using System; using System.Collections.Generic; using System.Text; using System.Xml; using System.Diagnostics; using System.Web; using TradeIdeas.ServerConnection; using TradeIdeas.XML; using TradeIdeas.MiscSupport; /* Consider using TopListRequest.cs rather than this API. */ namespace TradeIdeas.TIProData { public delegate void TopListData(List rows, DateTime? start, DateTime? end, TopList sender); public delegate void TopListStatus(TopList sender); public class TopListInfo { // This includes information about the request. As with the streaming alerts, we // get a short form of the config strings. But we get a lot more, like the list // of columns. The server tells us this information; we shouldn't guess at it. // Eventually we plan to change the streaming and hisorical alerts to work this // way, too. public string Config; public string WindowName; public string ServerSort; public IList Columns; public bool SameColumns(IList columns) { return (ColumnInfo.Equal(columns, Columns)); } public override string ToString() { StringBuilder result = new StringBuilder(); result.Append("TopListInfo(Config=\""); result.Append(Config); result.Append("\", WindowName=\""); result.Append(WindowName); result.Append('"'); foreach (ColumnInfo column in Columns) { result.Append(", "); column.DebugString(result); } result.Append(')'); return result.ToString(); } } public interface TopList { event TopListData TopListData; event TopListStatus TopListStatus; void Start(); void Stop(); // This will be null at first. After it gets filled in by the server we will send // a status update. It might get changed (followed by another status update) but it // will never change back to null. You will never get a data callback unless this // has been filled in. TopListInfo TopListInfo { get; } } public class TopListManager { private readonly object _mutex = new object(); private readonly Dictionary _windows = new Dictionary(); private class TopListImpl : TopList { private static Int64 _lastId = 0; private readonly string _id; private readonly bool _saveToMru; private TopListManager _manager; // The entire request is in the config string. This includes the data, and // also the history mode. private string _request; public TopListInfo TopListInfo { get; private set; } public TopListImpl(string request, bool saveToMru, TopListManager manager) { _saveToMru = saveToMru; lock (typeof(TopListImpl)) { _lastId++; _id = "TL" + _lastId.ToString(); } _request = request; _manager = manager; } public void SendRequestNow() { // Assume we are already in the mutex. List message = new List(); message.Add("command"); message.Add("top_list_start"); message.Add("window_id"); message.Add(_id); message.Add("long_form"); message.Add(_request); message.Add("non_filter_columns"); message.Add(1); message.Add("save_to_mru"); message.Add(_saveToMru ? "1" : "0"); _manager.OpenChannel(); _manager._serverConnection.SendMessage( TalkWithServer.CreateMessage(message.ToArray())); // Ignore the response. The server does not give us any interesting // information here. It will send a response when it is done with // the request, such as a history request or a request which was // canceled, but there is no interesting data in that message. } private bool _started = false; public void Start() { lock (_manager._mutex) { Debug.Assert(!_started, "Only call TopList.Start() once per object."); _started = true; _manager._windows.Add(_id, this); if (null != _manager._serverConnection) SendRequestNow(); } } public void Stop() { lock (_manager._mutex) { _started = true; if (_manager._windows.ContainsKey(_id)) { _manager._windows.Remove(_id); TalkWithServer serverConnection = _manager._serverConnection; if (null != serverConnection) serverConnection.SendMessage(TalkWithServer.CreateMessage( "command", "top_list_stop", "window_id", _id)); } } } public void ProcessServerMessage(XmlNode message) { switch (message.Property("TYPE")) { case "data": ProcessData(message); break; case "info": ProcessInfo(message); break; } } private static RowData DecodeRow(XmlNode asXml) { RowData result = new RowData(); foreach (XmlAttribute attribute in asXml.Attributes) result.Data[attribute.LocalName] = attribute.Value; return result; } private static List DecodeRows(XmlNode asXml) { List result = new List(); foreach (XmlNode rowNode in asXml.Enum()) result.Add(DecodeRow(rowNode)); return result; } private void ProcessData(XmlNode message) { if (null == TopListInfo) // Invariant: We will not send these messages to the consumer // out of order. This shouldn't happen but it could if the // server is acting up. Originally this class was responsible // for finding this problem and dealing with it because the // protocol was different and it made sense for us to resend the // request. Now it's not clear what to do. This at least // avoids a possible null pointer access. And it is consistent // with our normal policy of just filling in defaults for missing // data. ProcessInfo(null); if (message.Property("DONE") == "1") // This was a one time request. So we are done. lock (_manager._mutex) { _manager._windows.Remove(_id); } TopListData callback = TopListData; if (null != callback) try { DateTime? start = ServerFormats.DecodeServerTime(message.Property("START_TIME")); DateTime? end = ServerFormats.DecodeServerTime(message.Property("END_TIME")); callback(DecodeRows(message), start, end, this); } catch (Exception e) { string debugView = e.StackTrace; } } private void ProcessInfo(XmlNode message) { string clientCookie = message.Node("CLIENT_COOKIE").Property("VALUE"); // TODO -- Use this! _request = message.Property("SHORT_FORM", _request); TopListInfo newInfo = new TopListInfo(); newInfo.Config = _request; newInfo.WindowName = message.Property("WINDOW_NAME"); if (null != message.Node("SORT_BY")) newInfo.ServerSort = message.Node("SORT_BY").Property("FIELD"); List columns = new List(); //TODO: This is going away, but removing it is currently causing an exception //for parts of the code that assume the Symbol column will exist. //foreach (XmlNode columnXml in _manager._config.Node("TOP_LIST_COLUMNS").Enum()) // columns.Add(new ColumnInfo(columnXml)); foreach (XmlNode columnXml in message.Node("COLUMNS").Enum()) // add parameter for form type - RVH20210602 //columns.Add(new ColumnInfo(columnXml)); columns.Add(new ColumnInfo(columnXml, "TOP_LIST_WINDOW")); newInfo.Columns = columns.AsReadOnly(); // Note: Some or all of this might be a duplicate. TopListForm // looks for duplicate columns and does not do a full update in // this case. This code always sends the message, regardless of // what might or might not have changed. TopListInfo = newInfo; TopListStatus callback = TopListStatus; if (null != callback) callback(this); } public event TopListData TopListData; public event TopListStatus TopListStatus; } public TopList GetTopList(string request, bool saveToMru) { return new TopListImpl(request, saveToMru, this); } private TalkWithServer _serverConnection; private ConnectionLifeCycle _connectionLifeCycle; internal TopListManager(ConnectionLifeCycle connectionLifeCycle) { _connectionLifeCycle = connectionLifeCycle; connectionLifeCycle.OnConnection += new ConnectionLifeCycle.ConnectionCallback(connectionLifeCycle_OnConnection); } void connectionLifeCycle_OnConnection(ConnectionLifeCycle sender, TalkWithServer serverConnection) { lock (_mutex) { _channelIsOpen = false; if (_connectionLifeCycle.Status != ConnectionLifeCycle.ConnectionStatus.Full) // This is a streaming connection that never ends. We don't want to keep asking // for data when we are not logged in. History didn't need this case. If // you ask the server for history and you are not logged, it will immedialy fail. // That makes some sense for a one time request. If you want more hisory // you just request it again. The top list will automatically start and stop // as you log in and out again. _serverConnection = null; else { _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", "top_list_listen"), Response, streaming: true); } private int _deletedWindowDataCount; 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()) { TopListImpl 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); } } } // This protocol is based a lot on the HistoryManager, but it was designed after // HistoryManager.cs was written. The protocol was specifically written so the // C Sharp code would be simpler. It turns out that you can't do a lot better // than HistoryManager.cs. You need to listen for the new connections, and to // restart whenever the user is logged in and we reconnet. // // There is one channel for all data, as in History. These responses will // come whenever the server chooses to send them. That applies to both streaming // and one time messages. // // When you request a list, you send a message to the server. The server will // respond to that message *after* it is done. In the case of a one time message // the response will come after the data for that message. In the case of a // streaming message, it will come after the request has been canceled. // // Originally the client was going to listen for the responses to the original // requests. If he got one at an unexpected time (i.e. a one time message where // he got no data or a streaming message that he did not cancel) he can simply // resend the message. This was all designed to work with SendManager, and without // using ConnectionLIfeCycle. That didn't really work out. It turns out it's // simpler just to listen to the login message and only use that to restart. }