Unit MainConnectionUnit; Interface Uses WakeMeSoon, TalkWithServer, Classes, ExtCtrls; Type TAlertCallback = Procedure(Const Data : String) Of Object; TMainConnection = Class(TObject) Private FUsername, FPassword : String; FTcpIpServerAddress : String; FConnectionHalted : Boolean; FSessionId : String; { Used by the server to ensure that only one client is active at a time. This is a majic cookie to us, and not much more than a random number generator to the server. } Timer : TTimer; FInactiveCount : Integer; FSubscriptions : TStringList; Procedure DoTimer(Sender : TObject); Procedure AccountStatusResponse(Success : Boolean; Body : String; OriginalMessage : TMessageToServer); {Procedure AlertResponse(Success : Boolean; Body : String; OriginalMessage : TMessageToServer); } Constructor Create; Private ServerConnection : TTalkWithServer; ServerConnectWakup : TWakeMeSoon; Procedure CreateServerConnection; Procedure ResetServerConnection; Procedure BreakServerConnection; Procedure ConnectToServer; Procedure LogIntoServer; Procedure ServerMessagePreview(Success : Boolean; Body : String; OriginalMessage : TMessageToServer); Procedure ServerConnectionBroken; Procedure ServerConnectionAutoRetry(Msg : String); Procedure ServerConnectionTimerHandler; Function GetServerConnection(CreateIfRequired : Boolean = True) : TTalkWithServer; Public // Export these so we don't have to export our instance of TTalkWithServer. Function SendMessageToServer(Msg : TMessageToServer; Response : TOnMessageFromServer = Nil ) : TUniqueMessageId; Procedure AbandonServerMessage(Id : TUniqueMessageId); Private FTimerPrecision : Int64; FLastPingRequestTime : Int64; FLastPingRequestId : TUniqueMessageId; FNextPingRequestTime : TDateTime; Procedure PingServer; Procedure ResetPingTime; Procedure PingResponse(Success : Boolean; Body : String; OriginalMessage : TMessageToServer); Procedure InitialPingResponse(Success : Boolean; Body : String; OriginalMessage : TMessageToServer); Private FAlertListenerList : Array Of TAlertCallback; Public Procedure AddAlertListener(Callback : TAlertCallback); // AlertResponse only public to support debug option Procedure AlertResponse(Success : Boolean; Body : String; OriginalMessage : TMessageToServer); Protected Procedure SetUserName(Value : String); Procedure SetPassword(Value : String); Private FStatus : String; // The reason we were disconnected. FStatusCallback : TThreadMethod; Procedure SetStatus(Value : String); Public Property StatusCallback : TThreadMethod Read FStatusCallback Write FStatusCallback; Property Status : String Read FStatus; Public Class Function Instance : TMainConnection; Destructor Destroy; Override; Property ConnectionHalted : Boolean Read FConnectionHalted; Property UserName : String Read FUserName Write SetUserName; Property Password : String Read FPassword Write SetPassword; Function LoggedIn : Boolean; Procedure Reset; { Reconnect. Start fresh. Kick off other users if need be. } Procedure SoftReset; { Reconnect. Try to get back on exactly where we left off. Generally similar to what happens when there is a network error, and we have to reconnect. } Procedure Subscribe(Const Subscription : String); End; Implementation Uses SysUtils, Windows, IniFiles; Procedure TMainConnection.Subscribe(Const Subscription : String); Begin If Subscription <> '' Then FSubscriptions.Add(Subscription) End; Procedure TMainConnection.InitialPingResponse(Success : Boolean; Body : String; OriginalMessage : TMessageToServer); Begin If Success Then Begin SetStatus('Connected!'); //If Assigned(FConnectionStartedCallback) Then // FConnectionStartedCallback End End; Procedure TMainConnection.SetStatus(Value : String); Begin If FStatus <> Value Then Begin FStatus := Value; If Assigned(StatusCallback) Then Try StatusCallback Except End End End; Constructor TMainConnection.Create; Var IniFile : TMemIniFile; Begin ServerConnectWakup := TWakeMeSoon.Create; ServerConnectWakup.OnWakeUp := ConnectToServer; Timer := TTimer.Create(Nil); Timer.Interval := 500; Timer.OnTimer := DoTimer; Timer.Enabled := True; If Not QueryPerformanceFrequency(FTimerPrecision) Then FTimerPrecision := 0; // We always use the timer precision in the same way. We divide it by // 1000 to tell us how many ticks per millisecond. But we multiply it by // 2 because we start with the round trip time but we want to report the // one way time. We do the math once here, rather than doing it every // time we use it. FTimerPrecision := FTimerPrecision Div 500; FTcpIpServerAddress := 'weston.trade-ideas.com:9339'; IniFile := TMemIniFile.Create('TIQ.ini'); Try FTcpIpServerAddress := IniFile.ReadString('Connection', 'address', FTcpIpServerAddress); Finally IniFile.Free End; FSubscriptions := TStringList.Create; FSubscriptions.Sorted := True; FSubscriptions.Duplicates := dupIgnore End; Procedure TMainConnection.SoftReset; Begin FInactiveCount := 0; ResetServerConnection; //SetAccountStatus(asWaiting) End; Procedure TMainConnection.Reset; Begin FInactiveCount := 0; ResetServerConnection; FConnectionHalted := False; FSessionId := ''; SetStatus(''); //If Assigned(ConnectionStatusCallback) Then // ConnectionStatusCallback(cseReconnected, ''); //ServerUpdatesRequired.RequestWakeup End; Function TMainConnection.LoggedIn : Boolean; Begin Result := (FUserName <> '') And (FPassword <> '') End; Procedure TMainConnection.SetUserName(Value : String); Begin If (FUsername <> Value) Or ConnectionHalted Then Begin FUsername := Value; Reset End End; Procedure TMainConnection.SetPassword(Value : String); Begin If (FPassword <> Value) Or ConnectionHalted Then Begin FPassword := Value; Reset End End; Procedure TMainConnection.PingResponse(Success : Boolean; Body : String; OriginalMessage : TMessageToServer); Begin FInactiveCount := 0; SetStatus('Working: ' + TimeToStr(Now)); // TI Pro used this to measure the ping time. End; Procedure TMainConnection.ResetPingTime; Const // Ideally we'd ping every 5 seconds. We say 4.5. Assuming that the // timer goes off once a second, this will cause a ping every 5 seconds. // If we said exactly 5 here, then we'd take 6 seconds half the time // because we might be a millisecond early or late in the timer. Period = 1.0 / 24.0 / 60.0 / 60.0 * 4.5; Begin FNextPingRequestTime := Now + Period End; Procedure TMainConnection.PingServer; Begin { Make sure that the connection is up. If the network socket breaks in a bad way, we will not be notified until we try to send data. We also use this to measure and display the speed of the network. Unlike TI Pro, we always need to send this. Otherwise the server would think we were dead and would disconnect us. } If Assigned(ServerConnection) And (FTimerPrecision > 0) And (Now > FNextPingRequestTime) Then Begin // If we haven't recieved a response to the last ping yet, stop // listening for it. AbandonServerMessage(FLastPingRequestId); FLastPingRequestId := SendMessageToServer(CreateMessageToServer(['command','ping', 'response','1']), PingResponse); If Not QueryPerformanceCounter(FLastPingRequestTime) Then FLastPingRequestTime := 0; ResetPingTime End End; Procedure TMainConnection.AbandonServerMessage(Id : TUniqueMessageId); Begin If Assigned(ServerConnection) Then ServerConnection.AbandonMessage(Id) End; Function TMainConnection.SendMessageToServer (Msg : TMessageToServer; Response : TOnMessageFromServer = Nil ) : TUniqueMessageId; Begin Result := GetServerConnection.SendMessage(Msg, Response) End; Function TMainConnection.GetServerConnection(CreateIfRequired : Boolean = True) : TTalkWithServer; Begin If CreateIfRequired And Not Assigned(ServerConnection) Then Begin CreateServerConnection; ServerConnectWakup.RequestWakeup End; Result := ServerConnection End; Procedure TMainConnection.DoTimer(Sender : TObject); Begin ServerConnectionTimerHandler; If (Not ConnectionHalted) And LoggedIn Then { ConnectionHalted means that the server asked us not to reconnect, perhaps because of a bad password. If we are not logged in then we do not expect the server to send us regular data. In any of these cases, we should not try to ping the server or automatically reconnect. } Begin PingServer; Inc(FInactiveCount); If FInactiveCount > 24 Then SoftReset End End; Procedure TMainConnection.ServerConnectionTimerHandler; Begin If Assigned(ServerConnection) And (ServerConnection.ServerConnectionStatus = scsDisconnected) Then Begin ServerConnection.Free; ServerConnection := Nil; If LoggedIn And Not ConnectionHalted Then ServerConnectWakup.RequestWakeup End; End; Procedure TMainConnection.ServerConnectionAutoRetry(Msg : String); Begin // Used in TI Pro for status messages. This callback only comes from the // HTTP mode. End; Procedure TMainConnection.ServerConnectionBroken; Begin // This only happens when the server initiates the shutdown. // This test is ugly. We don't display a yellow tickmark if we don't // anticipate a retry. //If Assigned(ServerConnection) // And LoggedIn // And (Not ConnectionHalted) // And (ConnectionCount > 0) Then // ReportNetworkActivity(False, ServerConnection.ErrorMessage) End; Procedure TMainConnection.ServerMessagePreview(Success : Boolean; Body : String; OriginalMessage : TMessageToServer); Begin // In TI Pro this updated the status lights. End; Procedure TMainConnection.BreakServerConnection; Begin If Assigned(ServerConnection) Then Begin SetStatus('Disconnected.'); // This callback would be considered an error message. ServerConnection.OnDisconnected := Nil; // Message preview would show a yellow tick mark for each item in // the queue. ServerConnection.OnMessagePreview := Nil; ServerConnection.Free; ServerConnection := Nil End End; Procedure TMainConnection.CreateServerConnection; Begin ResetPingTime; ServerConnection := TTalkWithServer.Create; ServerConnection.OnMessagePreview := ServerMessagePreview; ServerConnection.OnDisconnected := ServerConnectionBroken; ServerConnection.OnAutoRetry := ServerConnectionAutoRetry; //ServerConnection.SocksHost := SocksHost; //ServerConnection.SocksPort := SocksPort; //ServerConnection.SocksUsername := SocksUserName; //ServerConnection.SocksPassword := SocksPassword; //ServerConnection.SocksVersion := SocksVersion; //ServerConnection.UseHttpTunnel := UseHttpTunnel; //ServerConnection.HttpTunnelAddress := HttpTunnelAddress; ServerConnection.TcpIpServerAddress := FTcpIpServerAddress; //ServerConnection.ProxyUserName := ProxyUserName; //ServerConnection.ProxyPassword := ProxyPassword; //ServerConnection.ProxyServer := ProxyServer; //ServerConnection.ProxyPort := ProxyPort; //ServerConnection.TimeoutMs := TimeoutMs; LogIntoServer End; Procedure TMainConnection.ResetServerConnection; Begin BreakServerConnection; ServerConnectWakup.RequestWakeup End; Procedure TMainConnection.LogIntoServer; Var I : Integer; Begin If LoggedIn And (Not ConnectionHalted) Then Begin ServerConnection.SendMessage( CreateMessageToServer(['command', 'login', 'username', FUsername, 'password', FPassword, 'session_id', FSessionID]), AccountStatusResponse, True); ServerConnection.SendMessage( CreateMessageToServer(['command', 'listen_for_alerts']), AlertResponse, True); For I := 0 To Pred(FSubscriptions.Count) Do ServerConnection.SendMessage( CreateMessageToServer(['command', 'add_alert_category', 'category', FSubscriptions[I]]), Nil); ServerConnection.SendMessage( CreateMessageToServer(['command','ping', 'response','1']), InitialPingResponse) End End; Procedure TMainConnection.ConnectToServer; Begin If Not Assigned(ServerConnection) Then CreateServerConnection; If (ServerConnection.ServerConnectionStatus = scsNew) Then ServerConnection.Connect End; Procedure TMainConnection.AccountStatusResponse( Success : Boolean; Body : String; OriginalMessage : TMessageToServer); Var BrokenDown : TStringList; Begin If Success Then Begin BrokenDown := TStringList.Create; Try BrokenDown.Text := Body; If BrokenDown[0] = 'STOP' Then Begin FConnectionHalted := True; If BrokenDown.Count > 1 Then SetStatus(BrokenDown[1]); // Let the server break the connection. Calling // BreakServerConnection here would cause problems. It would // be okay to set a flag so the timer handler would get it, // or using a WakeMeSoon, but that seems unnecessary. // //BreakServerConnection End Else If BrokenDown[0] = 'SESSION ID' Then Begin FSessionId := BrokenDown[1] End Except End End End; Procedure TMainConnection.AddAlertListener(Callback : TAlertCallback); Var Index : Integer; Begin Index := Length(FAlertListenerList); SetLength(FAlertListenerList, Succ(Index)); FAlertListenerList[Index] := Callback End; Procedure TMainConnection.AlertResponse( Success : Boolean; Body : String; OriginalMessage : TMessageToServer); Var I : Integer; Begin If Success Then For I := Low(FAlertListenerList) To High(FAlertListenerList) Do Try FAlertListenerList[I](Body) Except End End; Destructor TMainConnection.Destroy; Begin Assert(False, 'TODO') End; Var MainConnectionInstance : TMainConnection; Class Function TMainConnection.Instance : TMainConnection; Begin If Not Assigned(MainConnectionInstance) Then MainConnectionInstance := TMainConnection.Create; Result := MainConnectionInstance End; End.