{*******************************************************} { } { Responsive Software http://www.responsive.co.nz } { } { Copyright (c) 2003-2006 Responsive Software Limited } { } {*******************************************************} unit ServerCommunicatorUnit; interface uses Classes, ScktComp, SyncObjs; type TServerCommunicator = class; TMessagingData = class; TThreadInfoData = class; TDatabaseObjectLocks = class; // this class is used to provide server functions to connected clients // when the application is running in server mode TServerCommunicator = class private FServerSocket : TServerSocket; // used to coordinate thread access to global data FGlobalDataLock : TCriticalSection; // used to coordinate thread access to the database FDatabaseLock : TCriticalSection; // keeps count of active connections FConnectionCount : integer; FConnectionCountLock : TCriticalSection; // bytes received count FBytesReceivedCount : integer; FBytesReceivedCountLock : TCriticalSection; // bytes sent count FBytesSentCount : integer; FBytesSentCountLock : TCriticalSection; // flag to indicate critical update lock held on database FDatabaseLocked : boolean; FDatabaseLockedLock : TCriticalSection; // messaging data FMessagingData : TMessagingData; // thread information for display purposes FThreadInfoData : TThreadInfoData; // database object locks FDatabaseObjectLocks : TDatabaseObjectLocks; procedure GetThread (Sender : TObject; ClientSocket : TServerClientWinSocket; var SocketThread : TServerClientThread); procedure Log (Str : string); public constructor Create; destructor Destroy; override; function ConnectionCount : integer; procedure IncrementConnectionCount; procedure DecrementConnectionCount; function ThreadInfoString : string; function BytesReceivedCount : integer; procedure IncrementBytesReceivedCount (Value : integer); function BytesSentCount : integer; procedure IncrementBytesSentCount (Value : integer); procedure AcquireGlobalDataLock; procedure ReleaseGlobalDataLock; procedure AcquireDatabaseLock; procedure ReleaseDatabaseLock; function LockDatabase : boolean; procedure UnlockDatabase; procedure SendDataToWorkstations (Data : string); end; // this class is the thread spawned by the server socket // within the TServerCommunicator class // there is one created for each new connection received TConnectionThread = class(TServerClientThread) private FServerCommunicator : TServerCommunicator; // ref to TServerCommunicator object which spawned this thread procedure Log (Str : string); public constructor Create (CreateSuspended: Boolean; ASocket: TServerClientWinSocket; ServerCommunicator : TServerCommunicator); procedure ClientExecute; override; end; // this class is used to hold the messages for a single workstation/thread TThreadMessages = class private FThreadId : integer; FData : TStringList; public constructor Create (ThreadId : integer); destructor Destroy; override; procedure AddData (Data : string); function GetData : string; function ThreadId : integer; end; // this class is used to hold the messaging data being sent between the workstations TMessagingData = class private FList : TList; FListLock : TCriticalSection; public constructor Create; destructor Destroy; override; procedure RegisterThread (ThreadId : integer); procedure UnregisterThread (ThreadId : integer); procedure SendMessagingData (Data : string; ThreadId : integer); function ReceiveMessagingData (ThreadId : integer) : string; end; // this class is used to hold the information for a single workstation/thread TThreadInfo = class private FThreadId : integer; FStarted : TDateTime; FClientIPAddress : string; FUserName : string; FBytesSentCount : integer; FBytesReceivedCount : integer; public constructor Create (ThreadId : integer; Started : TDateTime; ClientIPAddress : string; UserName : string); function ThreadId : integer; function Started : TDateTime; function ClientIPAddress : string; function UserName : string; procedure IncrementBytesReceivedCount (Value : integer); procedure IncrementBytesSentCount (Value : integer); function AsString : string; end; // this class is used to hold the thread info data TThreadInfoData = class private FList : TList; FListLock : TCriticalSection; public constructor Create; destructor Destroy; override; procedure RegisterThread (ThreadId : integer; Started : TDateTime; ClientIPAddress : string; UserName : string); procedure UnregisterThread (ThreadId : integer); procedure IncrementBytesReceivedCount (ThreadId : integer; Value : integer); procedure IncrementBytesSentCount (ThreadId : integer; Value : integer); function AsString : string; end; // this class is used to hold the database object locks TDatabaseObjectLocks = class private FStringList : TStringList; FStringListLock : TCriticalSection; public constructor Create; destructor Destroy; override; function Lock (Id : int64; UserName : string) : string; procedure Unlock (Id : int64; UserName : string); procedure ClearAllLocks (UserName : string); end; implementation uses SysUtils, DateUtils, IBDatabase, Globals, GeneralUtilities, Utilities, ClientCommunicatorUnit, DatabaseManager, DatabaseObjects, ProxyDatabaseObjectCollectionUnit, Windows; {***** TServerCommunicator methods ********************************************} constructor TServerCommunicator.Create; begin // create a single server socket FServerSocket := TServerSocket.Create(nil); FServerSocket.Port := GlobalConfiguration.PortNumber; // create separate thread for each connection FServerSocket.ServerType := stThreadBlocking; // increase the thread cache size FServerSocket.ThreadCacheSize := 20; // set event handler FServerSocket.OnGetThread := GetThread; // create thread control objects FGlobalDataLock := TCriticalSection.Create; FDatabaseLock := TCriticalSection.Create; FConnectionCountLock := TCriticalSection.Create; FBytesReceivedCountLock := TCriticalSection.Create; FBytesSentCountLock := TCriticalSection.Create; FDatabaseLockedLock := TCriticalSection.Create; // create an instance of the messaging data class FMessagingData := TMessagingData.Create; // create an instance of the thread information class FThreadInfoData := TThreadInfoData.Create; // create an instance of the database object locks class FDatabaseObjectLocks := TDatabaseObjectLocks.Create; // open the socket to listen for incoming connections FServerSocket.Active := true; end; destructor TServerCommunicator.Destroy; begin // close connections FServerSocket.Close; // destroy server socket FServerSocket.Free; // destroy thread control objects FGlobalDataLock.Free; FDatabaseLock.Free; FConnectionCountLock.Free; FBytesReceivedCountLock.Free; FBytesSentCountLock.Free; FDatabaseLockedLock.Free; // destroy the messaging data FMessagingData.Free; // destroy the thread information FThreadInfoData.Free; // destroy the database object locks FDatabaseObjectLocks.Free; end; procedure TServerCommunicator.Log (Str : string); begin if ServerLogging then MessageLog.Log(Str); end; procedure TServerCommunicator.GetThread (Sender : TObject; ClientSocket : TServerClientWinSocket; var SocketThread : TServerClientThread); begin // create new thread for connection on request SocketThread := TConnectionThread.Create(false,ClientSocket,Self); end; function TServerCommunicator.ConnectionCount : integer; begin FConnectionCountLock.Acquire; try Result := FConnectionCount; finally FConnectionCountLock.Release; end; end; procedure TServerCommunicator.IncrementConnectionCount; begin FConnectionCountLock.Acquire; try Inc(FConnectionCount); finally FConnectionCountLock.Release; end; end; procedure TServerCommunicator.DecrementConnectionCount; begin FConnectionCountLock.Acquire; try Dec(FConnectionCount); finally FConnectionCountLock.Release; end; end; function TServerCommunicator.ThreadInfoString : string; begin Result := FThreadInfoData.AsString; end; function TServerCommunicator.BytesReceivedCount : integer; begin FBytesReceivedCountLock.Acquire; try Result := FBytesReceivedCount; finally FBytesReceivedCountLock.Release; end; end; procedure TServerCommunicator.IncrementBytesReceivedCount (Value : integer); begin FBytesReceivedCountLock.Acquire; try Inc(FBytesReceivedCount,Value); finally FBytesReceivedCountLock.Release; end; end; function TServerCommunicator.BytesSentCount : integer; begin FBytesSentCountLock.Acquire; try Result := FBytesSentCount; finally FBytesSentCountLock.Release; end; end; procedure TServerCommunicator.IncrementBytesSentCount (Value : integer); begin FBytesSentCountLock.Acquire; try Inc(FBytesSentCount,Value); finally FBytesSentCountLock.Release; end; end; procedure TServerCommunicator.AcquireGlobalDataLock; begin FGlobalDataLock.Acquire; end; procedure TServerCommunicator.ReleaseGlobalDataLock; begin FGlobalDataLock.Release; end; procedure TServerCommunicator.AcquireDatabaseLock; begin FDatabaseLock.Acquire; end; procedure TServerCommunicator.ReleaseDatabaseLock; begin FDatabaseLock.Release; end; function TServerCommunicator.LockDatabase : boolean; begin FDatabaseLockedLock.Acquire; try if not FDatabaseLocked then begin FDatabaseLocked := true; Result := true; end else Result := false; finally FDatabaseLockedLock.Release; end; end; procedure TServerCommunicator.UnlockDatabase; begin FDatabaseLockedLock.Acquire; try FDatabaseLocked := false; finally FDatabaseLockedLock.Release; end; end; procedure TServerCommunicator.SendDataToWorkstations (Data : string); var StringStream : TStringStream; Count : integer; begin StringStream := TStringStream.Create(''); try Count := 1; StringStream.Write(Count,SizeOf(Count)); WriteStrToStream(Data,StringStream); FMessagingData.SendMessagingData(StringStream.DataString,0); finally StringStream.Free; end; end; {***** TThreadMessages methods ************************************************} constructor TThreadMessages.Create (ThreadId : integer); begin FThreadId := ThreadId; FData := TStringList.Create; end; destructor TThreadMessages.Destroy; begin FData.Free; end; procedure TThreadMessages.AddData (Data : string); var StringList : TStringList; i : integer; StringStream : TStringStream; Count : integer; begin if Data <> '' then begin StringList := TStringList.Create; StringStream := TStringStream.Create(Data); try try StringStream.Position := 0; StringStream.Read(Count,SizeOf(Count)); for i := 0 to Count - 1 do StringList.Add(ReadStrFromStream(StringStream)); except StringList.Clear; end; for i := 0 to StringList.Count - 1 do FData.Add(StringList[i]); finally StringList.Free; StringStream.Free; end; end; end; function TThreadMessages.GetData : string; var StringStream : TStringStream; i : integer; Count : integer; begin StringStream := TStringStream.Create(''); try Count := FData.Count; StringStream.Write(Count,SizeOf(Count)); for i := 0 to FData.Count - 1 do WriteStrToStream(FData[i],StringStream); Result := StringStream.DataString; FData.Clear; finally StringStream.Free; end; end; function TThreadMessages.ThreadId : integer; begin Result := FThreadId; end; {***** TMessagingData methods *************************************************} constructor TMessagingData.Create; begin FListLock := TCriticalSection.Create; FList := TList.Create; end; destructor TMessagingData.Destroy; begin FListLock.Free; DestroyList(FList); end; procedure TMessagingData.RegisterThread (ThreadId : integer); var ThreadMessages : TThreadMessages; begin FListLock.Acquire; try ThreadMessages := TThreadMessages.Create(ThreadId); FList.Add(ThreadMessages); finally FListLock.Release; end; end; procedure TMessagingData.UnregisterThread (ThreadId : integer); var ThreadMessages : TThreadMessages; i : integer; begin FListLock.Acquire; try for i := 0 to FList.Count - 1 do begin ThreadMessages := TThreadMessages(FList[i]); if ThreadMessages.ThreadId = ThreadId then begin FList.Delete(i); ThreadMessages.Free; break; end; end; finally FListLock.Release; end; end; procedure TMessagingData.SendMessagingData (Data : string; ThreadId : integer); var ThreadMessages : TThreadMessages; i : integer; begin FListLock.Acquire; try // send the data to all connections except the one it came from for i := 0 to FList.Count - 1 do begin ThreadMessages := TThreadMessages(FList[i]); if ThreadMessages.ThreadId <> ThreadId then ThreadMessages.AddData(Data); end; finally FListLock.Release; end; end; function TMessagingData.ReceiveMessagingData (ThreadId : integer) : string; var ThreadMessages : TThreadMessages; i : integer; begin Result := ''; FListLock.Acquire; try for i := 0 to FList.Count - 1 do begin ThreadMessages := TThreadMessages(FList[i]); if ThreadMessages.ThreadId = ThreadId then begin Result := ThreadMessages.GetData; break; end; end; finally FListLock.Release; end; end; {***** TThreadInfo methods ************************************************} constructor TThreadInfo.Create (ThreadId : integer; Started : TDateTime; ClientIPAddress : string; UserName : string); begin FThreadId := ThreadId; FStarted := Started; FClientIPAddress := ClientIPAddress; FUserName := UserName; end; function TThreadInfo.ThreadId : integer; begin Result := FThreadId; end; function TThreadInfo.Started : TDateTime; begin Result := FStarted; end; function TThreadInfo.ClientIPAddress : string; begin Result := FClientIPAddress; end; function TThreadInfo.UserName : string; begin Result := FUserName; end; procedure TThreadInfo.IncrementBytesReceivedCount (Value : integer); begin Inc(FBytesReceivedCount,Value); end; procedure TThreadInfo.IncrementBytesSentCount (Value : integer); begin Inc(FBytesSentCount,Value); end; function TThreadInfo.AsString : string; begin Result := FormatDate(FStarted) + ' ' + FClientIPAddress + ' ' + FUserName; {+ ' ' + 'R=' + IntToStr(FBytesReceivedCount) + ' ' + 'S=' + IntToStr(FBytesSentCount);} end; {***** TThreadInfoData methods ************************************************} constructor TThreadInfoData.Create; begin FListLock := TCriticalSection.Create; FList := TList.Create; end; destructor TThreadInfoData.Destroy; begin FListLock.Free; DestroyList(FList); end; procedure TThreadInfoData.RegisterThread (ThreadId : integer; Started : TDateTime; ClientIPAddress : string; UserName : string); var ThreadInfo : TThreadInfo; begin FListLock.Acquire; try ThreadInfo := TThreadInfo.Create(ThreadId,Started,ClientIPAddress,UserName); FList.Add(ThreadInfo); finally FListLock.Release; end; end; procedure TThreadInfoData.UnregisterThread (ThreadId : integer); var ThreadInfo : TThreadInfo; i : integer; begin FListLock.Acquire; try for i := 0 to FList.Count - 1 do begin ThreadInfo := TThreadInfo(FList[i]); if ThreadInfo.ThreadId = ThreadId then begin FList.Delete(i); ThreadInfo.Free; break; end; end; finally FListLock.Release; end; end; procedure TThreadInfoData.IncrementBytesReceivedCount (ThreadId : integer; Value : integer); var ThreadInfo : TThreadInfo; i : integer; begin FListLock.Acquire; try for i := 0 to FList.Count - 1 do begin ThreadInfo := TThreadInfo(FList[i]); if ThreadInfo.ThreadId = ThreadId then begin ThreadInfo.IncrementBytesReceivedCount(Value); break; end; end; finally FListLock.Release; end; end; procedure TThreadInfoData.IncrementBytesSentCount (ThreadId : integer; Value : integer); var ThreadInfo : TThreadInfo; i : integer; begin FListLock.Acquire; try for i := 0 to FList.Count - 1 do begin ThreadInfo := TThreadInfo(FList[i]); if ThreadInfo.ThreadId = ThreadId then begin ThreadInfo.IncrementBytesSentCount(Value); break; end; end; finally FListLock.Release; end; end; function TThreadInfoData.AsString : string; var ThreadInfo : TThreadInfo; i : integer; begin Result := ''; FListLock.Acquire; try for i := FList.Count - 1 downto 0 do begin ThreadInfo := TThreadInfo(FList[i]); Result := Result + ThreadInfo.AsString + Chr($0D) + Chr($0A); end; finally FListLock.Release; end; end; {***** TDatabaseObjectLock methods ********************************************} constructor TDatabaseObjectLocks.Create; begin FStringListLock := TCriticalSection.Create; FStringList := TStringList.Create; end; destructor TDatabaseObjectLocks.Destroy; begin FStringListLock.Free; FStringList.Free; end; function TDatabaseObjectLocks.Lock (Id : int64; UserName : string) : string; var i : integer; begin FStringListLock.Acquire; try // check that object is not already locked // if so return the other user name or // an empty string if this user for i := 0 to FStringList.Count - 1 do if integer(FStringList.Objects[i]) = Id then begin if FStringList[i] <> UserName then Result := FStringList[i] else Result := ''; Exit; end; // add a new lock FStringList.AddObject(UserName,pointer(Id)); Result := ''; finally FStringListLock.Release; end; end; procedure TDatabaseObjectLocks.Unlock (Id : int64; UserName : string); var i : integer; begin FStringListLock.Acquire; try // unlock object only if it has been locked by this user for i := 0 to FStringList.Count - 1 do if integer(FStringList.Objects[i]) = Id then if FStringList[i] = UserName then begin FStringList.Delete(i); Exit; end; finally FStringListLock.Release; end; end; procedure TDatabaseObjectLocks.ClearAllLocks (UserName : string); var i : integer; begin FStringListLock.Acquire; try // unlock all objects locked by this user for i := FStringList.Count - 1 downto 0 do if FStringList[i] = UserName then FStringList.Delete(i); finally FStringListLock.Release; end; end; {***** TConnectionThread methods **********************************************} constructor TConnectionThread.Create (CreateSuspended: Boolean; ASocket: TServerClientWinSocket; ServerCommunicator : TServerCommunicator); begin FServerCommunicator := ServerCommunicator; inherited Create(CreateSuspended,ASocket); end; procedure TConnectionThread.Log (Str : string); begin FServerCommunicator.Log('TID = ' + IntToStr(ThreadId) + ' ' + Str); end; procedure TConnectionThread.ClientExecute; const MaxBytesRead = 1024; var WinSocketStream : TWinSocketStream; Buffer : string; ExpectedLengthStr : string; ExpectedLength : integer; ReceivedData : string; ReceivedString : string; BytesRead : integer; StartTime : TDateTime; UserName : string; DatabaseLocked : boolean; DatabaseLockTime : TDateTime; DatabaseLockCount : integer; // allow remote datasets to be nested ProxyDatabaseObjectCollection : array[1..10] of TProxyDatabaseObjectCollection; CurrentProxyDatabaseObjectCollection : integer; i : integer; procedure LockDatabase; begin if DatabaseLockCount = 0 then begin // wait in a loop until the database is locked while not FServerCommunicator.LockDatabase do Sleep(1); DatabaseLocked := true; end; Inc(DatabaseLockCount); DatabaseLockTime := Now; end; procedure UnlockDatabase; begin if DatabaseLockCount > 0 then Dec(DatabaseLockCount); if DatabaseLockCount = 0 then begin FServerCommunicator.UnlockDatabase; DatabaseLocked := false; end; end; procedure SendDataToClient (Data : string); var Str : string; BytesSent : integer; begin // prepend the data length in curly brackets so that the // receiving socket knows how much data to expect FServerCommunicator.AcquireGlobalDataLock; try Str := '{' + IntToStr(Length(Data)+1) + '}' + Encrypt(Data+TerminatingChar,ClientServerEncryptRandSeed); finally FServerCommunicator.ReleaseGlobalDataLock; end; BytesSent := WinSocketStream.Write(Str[1],Length(Str)); // record byte count FServerCommunicator.IncrementBytesSentCount(BytesSent); FServerCommunicator.FThreadInfoData.IncrementBytesSentCount(abs(ThreadId),BytesSent); if not BytesSent = Length(Str) then raise Exception.Create('Incomplete data sent to client'); end; procedure ProcessProgramVersion; var Str : string; begin FServerCommunicator.AcquireGlobalDataLock; try Str := ProgramVersion + ProgramName; finally FServerCommunicator.ReleaseGlobalDataLock; end; // return the current program version to the client SendDataToClient(Str); end; procedure ProcessFirebird; var Str : string; begin FServerCommunicator.AcquireGlobalDataLock; try if Firebird then Str := '1' else Str := '0'; finally FServerCommunicator.ReleaseGlobalDataLock; end; // return result to the client SendDataToClient(Str); end; procedure ProcessNotifyLoggedOnUser; begin UserName := Copy(ReceivedString,3,Length(ReceivedString)); FServerCommunicator.FThreadInfoData.RegisterThread (abs(ThreadId),Now,ClientSocket.RemoteAddress,UserName); Log(UserName + ' logged in'); SendDataToClient('0'); end; procedure ProcessNotifyInvalidLogin; var StringStream : TStringStream; UserName : string; Password : string; begin StringStream := TStringStream.Create(ReceivedString); try StringStream.Position := 2; UserName := ReadStrFromStream(StringStream); Password := ReadStrFromStream(StringStream); finally StringStream.Free; end; Log('Invalid login - User Name: ' + UserName + ' Password: ' + Password); SendDataToClient('0'); end; procedure ProcessMessagingData; var Str : string; begin Str := Copy(ReceivedString,3,Length(ReceivedString)); if Str <> '' then FServerCommunicator.FMessagingData.SendMessagingData(Str,abs(ThreadId)); Str := FServerCommunicator.FMessagingData.ReceiveMessagingData(abs(ThreadId)); SendDataToClient(Str); end; procedure ProcessCloseConnection; begin SendDataToClient('0'); Log('Server thread terminated - by client'); Terminate; Exit; end; procedure ProcessLoadGlobalConfiguration; var StringStream : TStringStream; Str : string; begin // in the Firebird database we cannot open a table in // exclusive mode as we can in the BDE // so we need to obtain a lock here first if Firebird then LockDatabase; try FServerCommunicator.AcquireGlobalDataLock; try LoadGlobalConfiguration; finally FServerCommunicator.ReleaseGlobalDataLock; end; finally if Firebird then UnlockDatabase; end; StringStream := TStringStream.Create(''); try FServerCommunicator.AcquireGlobalDataLock; try GlobalConfiguration.SaveToStream(StringStream); finally FServerCommunicator.ReleaseGlobalDataLock; end; Str := StringStream.DataString; finally StringStream.Free; end; SendDataToClient(Str); end; procedure ProcessSaveGlobalConfiguration; var StringStream : TStringStream; begin StringStream := TStringStream.Create(ReceivedString); try StringStream.Position := 2; FServerCommunicator.AcquireGlobalDataLock; try GlobalConfiguration.LoadFromStream(StringStream); finally FServerCommunicator.ReleaseGlobalDataLock; end; finally StringStream.Free; end; // in the Firebird database we cannot open a table in // exclusive mode as we can in the BDE // so we need to obtain a lock here first if Firebird then LockDatabase; try FServerCommunicator.AcquireGlobalDataLock; try SaveGlobalConfiguration; finally FServerCommunicator.ReleaseGlobalDataLock; end; finally if Firebird then UnlockDatabase; end; SendDataToClient('0'); end; procedure ProcessNoOfLoggedOnWorkstations; var Str : string; begin Str := IntToStr(FServerCommunicator.ConnectionCount); SendDataToClient(Str); end; procedure ProcessNoOfWorkstations; var Str : string; begin Str := IntToStr(NoOfWorkstations); SendDataToClient(Str); end; procedure ProcessPopulateStringsWithDistinctValues; var StringStream : TStringStream; TableName : string; FieldName : string; Str : string; ReturnedStrings : TStringList; begin StringStream := TStringStream.Create(ReceivedString); try StringStream.Position := 2; TableName := ReadStrFromStream(StringStream); FieldName := ReadStrFromStream(StringStream); finally StringStream.Free; end; ReturnedStrings := TStringList.Create; StringStream := TStringStream.Create(''); try Utilities.PopulateStringsWithDistinctValues (TableName,FieldName,ReturnedStrings,false); ReturnedStrings.SaveToStream(StringStream); Str := StringStream.DataString; finally ReturnedStrings.Free; StringStream.Free; end; SendDataToClient(Str); end; procedure ProcessDatabaseStatusString; var Str : string; begin Str := DatabaseStatusString; SendDataToClient(Str); end; procedure ProcessAcquireDatabaseCriticalUpdate; begin LockDatabase; SendDataToClient('0'); end; procedure ProcessReleaseDatabaseCriticalUpdate; begin UnlockDatabase; SendDataToClient('0'); end; procedure ProcessGenerateId; var Increment : integer; Str : string; begin Increment := StrToIntDef(Copy(ReceivedString,3,Length(ReceivedString)),1); // in the Firebird database we cannot open a table in // exclusive mode as we can in the BDE // so we need to obtain a lock here first if Firebird then LockDatabase; try Str := IntToStr(GenerateId(Increment)); finally if Firebird then UnlockDatabase; end; SendDataToClient(Str); end; procedure ProcessLoadDatabaseObjectById; var DatabaseObjectClass : TDatabaseObjectClass; Id : int64; DatabaseObject : TDatabaseObject; StringStream : TStringStream; Str : string; begin FServerCommunicator.AcquireGlobalDataLock; try DatabaseObjectClass := ConvertIntegerToDatabaseObjectClass(StrToIntDef(Copy(ReceivedString,3,3),-1)); finally FServerCommunicator.ReleaseGlobalDataLock; end; Id := StrToInt64Def(Copy(ReceivedString,6,Length(ReceivedString)),-1); DatabaseObject := LoadDatabaseObject(DatabaseObjectClass,Id); StringStream := TStringStream.Create(''); try if DatabaseObject <> nil then begin DatabaseObject.SaveToStream(StringStream); Str := StringStream.DataString; end else Str := '-1'; finally DatabaseObject.Free; StringStream.Free; end; SendDataToClient(Str); end; procedure ProcessLoadDatabaseObjectBySelectionString; var DatabaseObjectClass : TDatabaseObjectClass; SelectionString : string; DatabaseObject : TDatabaseObject; StringStream : TStringStream; Str : string; begin FServerCommunicator.AcquireGlobalDataLock; try DatabaseObjectClass := ConvertIntegerToDatabaseObjectClass(StrToIntDef(Copy(ReceivedString,3,3),-1)); finally FServerCommunicator.ReleaseGlobalDataLock; end; SelectionString := Copy(ReceivedString,6,Length(ReceivedString)); DatabaseObject := LoadDatabaseObject(DatabaseObjectClass,SelectionString); StringStream := TStringStream.Create(''); try if DatabaseObject <> nil then begin DatabaseObject.SaveToStream(StringStream); Str := StringStream.DataString; end else Str := '-1'; finally DatabaseObject.Free; StringStream.Free; end; SendDataToClient(Str); end; procedure ProcessSaveDatabaseObject; var DatabaseObjectClass : TDatabaseObjectClass; StringStream : TStringStream; DatabaseObject : TDatabaseObject; begin FServerCommunicator.AcquireGlobalDataLock; try DatabaseObjectClass := ConvertIntegerToDatabaseObjectClass(StrToIntDef(Copy(ReceivedString,3,3),-1)); finally FServerCommunicator.ReleaseGlobalDataLock; end; StringStream := TStringStream.Create(ReceivedString); try StringStream.Position := 5; DatabaseObject := DatabaseObjectClass.Create; DatabaseObject.LoadFromStream(StringStream); finally StringStream.Free; end; try DatabaseObject.SaveToDatabase(false); finally DatabaseObject.Free; end; SendDataToClient('0'); end; procedure ProcessDeleteDatabaseObject; var DatabaseObjectClass : TDatabaseObjectClass; Id : int64; DatabaseObject : TDatabaseObject; begin FServerCommunicator.AcquireGlobalDataLock; try DatabaseObjectClass := ConvertIntegerToDatabaseObjectClass(StrToIntDef(Copy(ReceivedString,3,3),-1)); finally FServerCommunicator.ReleaseGlobalDataLock; end; Id := StrToInt64Def(Copy(ReceivedString,6,Length(ReceivedString)),-1); DatabaseObject := LoadDatabaseObject(DatabaseObjectClass,Id); try if DatabaseObject <> nil then DatabaseObject.DeleteFromDatabase(false); finally DatabaseObject.Free; end; SendDataToClient('0'); end; procedure ProcessFullSaveDatabaseObject; var DatabaseObjectClass : TDatabaseObjectClass; StringStream : TStringStream; DatabaseObject : TDatabaseObject; IncludeDetails : boolean; NewObject : boolean; begin FServerCommunicator.AcquireGlobalDataLock; try DatabaseObjectClass := ConvertIntegerToDatabaseObjectClass(StrToIntDef(Copy(ReceivedString,3,3),-1)); finally FServerCommunicator.ReleaseGlobalDataLock; end; IncludeDetails := (Copy(ReceivedString,6,1) = 'Y'); NewObject := (Copy(ReceivedString,7,1) = 'Y'); StringStream := TStringStream.Create(ReceivedString); try StringStream.Position := 7; DatabaseObject := DatabaseObjectClass.Create; DatabaseObject.LoadFromStream(StringStream); if IncludeDetails then DatabaseObject.LoadDetailsFromStream(StringStream); finally StringStream.Free; end; try LockDatabase; DatabaseObject.FullSaveToDatabase(IncludeDetails,NewObject); finally UnlockDatabase; DatabaseObject.Free; end; SendDataToClient('0'); end; procedure ProcessLoadAllDatabaseObjects; var DatabaseObjectClass : TDatabaseObjectClass; DatabaseObjectCollection : TDatabaseObjectCollection; StringStream : TStringStream; Str : string; begin FServerCommunicator.AcquireGlobalDataLock; try DatabaseObjectClass := ConvertIntegerToDatabaseObjectClass(StrToIntDef(Copy(ReceivedString,3,3),-1)); finally FServerCommunicator.ReleaseGlobalDataLock; end; DatabaseObjectCollection := nil; LoadAllDatabaseObjects(DatabaseObjectCollection,DatabaseObjectClass); StringStream := TStringStream.Create(''); try DatabaseObjectCollection.SaveToStream(StringStream); Str := StringStream.DataString; finally DatabaseObjectCollection.Free; StringStream.Free; end; SendDataToClient(Str); end; procedure ProcessLoadSomeDatabaseObjects; var DatabaseObjectClass : TDatabaseObjectClass; SelectionString : string; DatabaseObjectCollection : TDatabaseObjectCollection; StringStream : TStringStream; Str : string; begin FServerCommunicator.AcquireGlobalDataLock; try DatabaseObjectClass := ConvertIntegerToDatabaseObjectClass(StrToIntDef(Copy(ReceivedString,3,3),-1)); finally FServerCommunicator.ReleaseGlobalDataLock; end; SelectionString := Copy(ReceivedString,6,Length(ReceivedString)); DatabaseObjectCollection := nil; LoadSomeDatabaseObjects(DatabaseObjectCollection,DatabaseObjectClass,SelectionString); StringStream := TStringStream.Create(''); try DatabaseObjectCollection.SaveToStream(StringStream); Str := StringStream.DataString; finally DatabaseObjectCollection.Free; StringStream.Free; end; SendDataToClient(Str); end; procedure ProcessCountDatabaseObjects; var DatabaseObjectClass : TDatabaseObjectClass; SelectionString : string; Str : string; begin FServerCommunicator.AcquireGlobalDataLock; try DatabaseObjectClass := ConvertIntegerToDatabaseObjectClass(StrToIntDef(Copy(ReceivedString,3,3),-1)); finally FServerCommunicator.ReleaseGlobalDataLock; end; SelectionString := Copy(ReceivedString,6,Length(ReceivedString)); Str := IntToStr(CountDatabaseObjects(DatabaseObjectClass,SelectionString)); SendDataToClient(Str); end; procedure ProcessSaveDatabaseObjects; var StringStream : TStringStream; DatabaseObjectClass : TDatabaseObjectClass; DatabaseObjectCollection : TDatabaseObjectCollection; begin FServerCommunicator.AcquireGlobalDataLock; try DatabaseObjectClass := ConvertIntegerToDatabaseObjectClass(StrToIntDef(Copy(ReceivedString,3,3),-1)); finally FServerCommunicator.ReleaseGlobalDataLock; end; StringStream := TStringStream.Create(ReceivedString); try StringStream.Position := 5; DatabaseObjectCollection := TDatabaseObjectCollection.Create; DatabaseObjectCollection.Owned := true; DatabaseObjectCollection.LoadFromStream(StringStream); finally StringStream.Free; end; try DatabaseObjectCollection.SaveToDatabase(DatabaseObjectClass,false); finally DatabaseObjectCollection.Free; end; SendDataToClient('0'); end; procedure ProcessDeleteDatabaseObjects; var StringStream : TStringStream; DatabaseObjectClass : TDatabaseObjectClass; DatabaseObjectCollection : TDatabaseObjectCollection; begin FServerCommunicator.AcquireGlobalDataLock; try DatabaseObjectClass := ConvertIntegerToDatabaseObjectClass(StrToIntDef(Copy(ReceivedString,3,3),-1)); finally FServerCommunicator.ReleaseGlobalDataLock; end; StringStream := TStringStream.Create(ReceivedString); try StringStream.Position := 5; DatabaseObjectCollection := TDatabaseObjectCollection.Create; DatabaseObjectCollection.Owned := true; DatabaseObjectCollection.LoadFromStream(StringStream); finally StringStream.Free; end; try DatabaseObjectCollection.DeleteFromDatabase(DatabaseObjectClass,false); finally DatabaseObjectCollection.Free; end; SendDataToClient('0'); end; procedure ProcessDeleteSomeDatabaseObjects; var DatabaseObjectClass : TDatabaseObjectClass; SelectionString : string; begin FServerCommunicator.AcquireGlobalDataLock; try DatabaseObjectClass := ConvertIntegerToDatabaseObjectClass(StrToIntDef(Copy(ReceivedString,3,3),-1)); finally FServerCommunicator.ReleaseGlobalDataLock; end; SelectionString := Copy(ReceivedString,6,Length(ReceivedString)); DeleteSomeDatabaseObjects(DatabaseObjectClass,SelectionString); SendDataToClient('0'); end; procedure ProcessDeleteAndSaveSomeDatabaseObjects; var StringStream : TStringStream; DatabaseObjectClass : TDatabaseObjectClass; SelectionString : string; DatabaseObjectCollection : TDatabaseObjectCollection; begin FServerCommunicator.AcquireGlobalDataLock; try DatabaseObjectClass := ConvertIntegerToDatabaseObjectClass(StrToIntDef(Copy(ReceivedString,3,3),-1)); finally FServerCommunicator.ReleaseGlobalDataLock; end; StringStream := TStringStream.Create(ReceivedString); try StringStream.Position := 5; SelectionString := ReadStrFromStream(StringStream); DatabaseObjectCollection := TDatabaseObjectCollection.Create; DatabaseObjectCollection.Owned := true; DatabaseObjectCollection.LoadFromStream(StringStream); finally StringStream.Free; end; try DeleteAndSaveSomeDatabaseObjects(DatabaseObjectClass,SelectionString,DatabaseObjectCollection,false); finally DatabaseObjectCollection.Free; end; SendDataToClient('0'); end; procedure ProcessUpdateSomeDatabaseObjects; var DatabaseObjectClass : TDatabaseObjectClass; StringStream : TStringStream; UpdateString : string; SelectionString : string; begin FServerCommunicator.AcquireGlobalDataLock; try DatabaseObjectClass := ConvertIntegerToDatabaseObjectClass(StrToIntDef(Copy(ReceivedString,3,3),-1)); finally FServerCommunicator.ReleaseGlobalDataLock; end; StringStream := TStringStream.Create(ReceivedString); try StringStream.Position := 5; UpdateString := ReadStrFromStream(StringStream); SelectionString := ReadStrFromStream(StringStream); finally StringStream.Free; end; UpdateSomeDatabaseObjects(DatabaseObjectClass,UpdateString,SelectionString); SendDataToClient('0'); end; procedure ProcessOpenRemoteDataset; var DatabaseObjectClass : TDatabaseObjectClass; SelectionString : string; Str : string; begin FServerCommunicator.AcquireGlobalDataLock; try DatabaseObjectClass := ConvertIntegerToDatabaseObjectClass(StrToIntDef(Copy(ReceivedString,3,3),-1)); finally FServerCommunicator.ReleaseGlobalDataLock; end; SelectionString := Copy(ReceivedString,6,Length(ReceivedString)); Inc(CurrentProxyDatabaseObjectCollection); ProxyDatabaseObjectCollection[CurrentProxyDatabaseObjectCollection] := TProxyDatabaseObjectCollection.Create (DatabaseObjectClass,nil,SelectionString); Str := IntToStr(ProxyDatabaseObjectCollection[CurrentProxyDatabaseObjectCollection].Count); SendDataToClient(Str); end; procedure ProcessLoadDatabaseObjectsFromRemoteDataset; var StartRecNo : integer; EndRecNo : integer; DatabaseObjectCollection : TDatabaseObjectCollection; StringStream : TStringStream; Str : string; i : integer; DatabaseObject : TDatabaseObject; begin StringStream := TStringStream.Create(ReceivedString); try StringStream.Position := 2; StringStream.Read(StartRecNo,SizeOf(StartRecNo)); StringStream.Read(EndRecNo,SizeOf(EndRecNo)); finally StringStream.Free; end; DatabaseObjectCollection := TDatabaseObjectCollection.Create; DatabaseObjectCollection.Owned := false; for i := StartRecNo - 1 to EndRecNo - 1 do begin DatabaseObject := ProxyDatabaseObjectCollection[CurrentProxyDatabaseObjectCollection][i]; if DatabaseObject <> nil then DatabaseObjectCollection.Add(DatabaseObject); end; StringStream := TStringStream.Create(''); try DatabaseObjectCollection.SaveToStream(StringStream); Str := StringStream.DataString; finally DatabaseObjectCollection.Free; StringStream.Free; end; SendDataToClient(Str); end; procedure ProcessCloseRemoteDataset; begin ProxyDatabaseObjectCollection[CurrentProxyDatabaseObjectCollection].Free; ProxyDatabaseObjectCollection[CurrentProxyDatabaseObjectCollection] := nil; Dec(CurrentProxyDatabaseObjectCollection); SendDataToClient('0'); end; procedure ProcessLockDatabaseObject; var Id : int64; OtherUserName : string; begin Id := StrToInt64Def(Copy(ReceivedString,3,Length(ReceivedString)),-1); OtherUserName := FServerCommunicator.FDatabaseObjectLocks.Lock(Id,UserName); SendDataToClient(OtherUserName); end; procedure ProcessUnlockDatabaseObject; var Id : int64; begin Id := StrToInt64Def(Copy(ReceivedString,3,Length(ReceivedString)),-1); FServerCommunicator.FDatabaseObjectLocks.Unlock(Id,UserName); SendDataToClient('0'); end; procedure ProcessGetAttachmentFileAge; var AttachmentId : int64; FileName : string; FileAge : integer; Str : string; begin AttachmentId := StrToInt64Def(Copy(ReceivedString,3,Length(ReceivedString)),-1); FServerCommunicator.AcquireGlobalDataLock; try FileName := AppendBackslash(GlobalConfiguration.AttachmentDirectory) + IntToStr(AttachmentId); finally FServerCommunicator.ReleaseGlobalDataLock; end; if FileExists(FileName) then FileAge := SysUtils.FileAge(FileName) else FileAge := 0; Str := IntToStr(FileAge); SendDataToClient(Str); end; procedure ProcessGetAttachmentFileContents; var AttachmentId : int64; FileName : string; Str : string; begin AttachmentId := StrToInt64Def(Copy(ReceivedString,3,Length(ReceivedString)),-1); FServerCommunicator.AcquireGlobalDataLock; try FileName := AppendBackslash(GlobalConfiguration.AttachmentDirectory) + IntToStr(AttachmentId); finally FServerCommunicator.ReleaseGlobalDataLock; end; if FileExists(FileName) then Str := GeneralUtilities.GetFileContents(FileName) else Str := ''; SendDataToClient(CompressString(Str)); end; procedure ProcessSetServerThreadPriority; var Priority : TThreadPriority; begin Priority := TThreadPriority(StrToIntDef(Copy(ReceivedString,3,Length(ReceivedString)),integer(tpNormal))); Self.Priority := Priority; SendDataToClient('0'); end; procedure ProcessInvalidRequest; begin raise Exception.Create('Invalid request received from client'); end; procedure ProcessReceivedString; var ClientRequestType : TClientRequestType; begin ClientRequestType := TClientRequestType(StrToIntDef(Copy(ReceivedString,1,2),-1)); case ClientRequestType of crtProgramVersion : ProcessProgramVersion; crtFirebird : ProcessFirebird; crtNotifyLoggedOnUser : ProcessNotifyLoggedOnUser; crtNotifyInvalidLogin : ProcessNotifyInvalidLogin; crtMessagingData : ProcessMessagingData; crtCloseConnection : ProcessCloseConnection; crtLoadGlobalConfiguration : ProcessLoadGlobalConfiguration; crtSaveGlobalConfiguration : ProcessSaveGlobalConfiguration; crtNoOfLoggedOnWorkstations : ProcessNoOfLoggedOnWorkstations; crtNoOfWorkstations : ProcessNoOfWorkstations; crtPopulateStringsWithDistinctValues : ProcessPopulateStringsWithDistinctValues; crtDatabaseStatusString : ProcessDatabaseStatusString; crtAcquireDatabaseCriticalUpdate : ProcessAcquireDatabaseCriticalUpdate; crtReleaseDatabaseCriticalUpdate : ProcessReleaseDatabaseCriticalUpdate; crtGenerateId : ProcessGenerateId; crtLoadDatabaseObjectById : ProcessLoadDatabaseObjectById; crtLoadDatabaseObjectBySelectionString : ProcessLoadDatabaseObjectBySelectionString; crtSaveDatabaseObject : ProcessSaveDatabaseObject; crtDeleteDatabaseObject : ProcessDeleteDatabaseObject; crtFullSaveDatabaseObject : ProcessFullSaveDatabaseObject; crtLoadAllDatabaseObjects : ProcessLoadAllDatabaseObjects; crtLoadSomeDatabaseObjects : ProcessLoadSomeDatabaseObjects; crtCountDatabaseObjects : ProcessCountDatabaseObjects; crtSaveDatabaseObjects : ProcessSaveDatabaseObjects; crtDeleteDatabaseObjects : ProcessDeleteDatabaseObjects; crtDeleteSomeDatabaseObjects : ProcessDeleteSomeDatabaseObjects; crtDeleteAndSaveSomeDatabaseObjects : ProcessDeleteAndSaveSomeDatabaseObjects; crtUpdateSomeDatabaseObjects : ProcessUpdateSomeDatabaseObjects; crtOpenRemoteDataset : ProcessOpenRemoteDataset; crtLoadDatabaseObjectsFromRemoteDataset : ProcessLoadDatabaseObjectsFromRemoteDataset; crtCloseRemoteDataset : ProcessCloseRemoteDataset; crtLockDatabaseObject : ProcessLockDatabaseObject; crtUnlockDatabaseObject : ProcessUnlockDatabaseObject; crtGetAttachmentFileAge : ProcessGetAttachmentFileAge; crtGetAttachmentFileContents : ProcessGetAttachmentFileContents; crtSetServerThreadPriority : ProcessSetServerThreadPriority; else ProcessInvalidRequest; end; end; begin Log('Server thread started'); // record reference to current thread in thread local space TVThread := Self; // update the connection count FServerCommunicator.IncrementConnectionCount; // register thread with messaging data class FServerCommunicator.FMessagingData.RegisterThread(abs(ThreadId)); // create instance of TWinSocketStream for this connection WinSocketStream := TWinSocketStream.Create(ClientSocket,ServerTimeout); // create a new session for database access FServerCommunicator.AcquireDatabaseLock; try if not Firebird then begin TVSessionName := GetFreeSessionName; TVFirebirdDatabase := nil; end else begin TVSessionName := ''; TVFirebirdDatabase := TIBDatabase.Create(nil); TVFirebirdDatabase.DatabaseName := FirebirdServerIPAddress + ':' + FirebirdDatabaseName; TVFirebirdDatabase.Params.Add('user_name=' + FirebirdUsername); TVFirebirdDatabase.Params.Add('password=' + FirebirdPassword); TVFirebirdDatabase.LoginPrompt := false; TVFirebirdDatabase.Open; end; finally FServerCommunicator.ReleaseDatabaseLock; end; // initialise other local variables CurrentProxyDatabaseObjectCollection := 0; for i := Low(ProxyDatabaseObjectCollection) to High(ProxyDatabaseObjectCollection) do ProxyDatabaseObjectCollection[i] := nil; ExpectedLength := 0; ReceivedData := ''; BytesRead := 0; StartTime := Now; DatabaseLocked := false; DatabaseLockCount := 0; try while true do begin // check connection and if disconnected then terminate the thread if not ClientSocket.Connected then begin Log('Server thread terminated - connection lost'); Terminate; Exit; end; // check for dead client and terminate if Now > IncMilliSecond(StartTime,ServerTimeout) then begin Log('Server thread terminated - no communication from client'); Terminate; Exit; end; // initialise receive buffer SetLength(Buffer,MaxBytesRead); FillChar(Buffer[1],MaxBytesRead,0); // read data from socket try if WinSocketStream.WaitForData(ServerTimeout) then BytesRead := WinSocketStream.Read(Buffer[1], MaxBytesRead) else BytesRead := 0; except // if an exception occurs while reading then give up // and log error message on E:Exception do begin Log('Server thread terminated - ' + E.Message); Terminate; Exit; end; end; // check that we have not locked the database for too long if DatabaseLocked and (Now > IncMilliSecond(DatabaseLockTime,MaxDatabaseLockTime)) then begin FServerCommunicator.UnlockDatabase; DatabaseLockCount := 0; DatabaseLocked := false; end; // process data received if BytesRead > 0 then begin SetLength(Buffer,BytesRead); // record byte count FServerCommunicator.IncrementBytesReceivedCount(BytesRead); FServerCommunicator.FThreadInfoData.IncrementBytesReceivedCount(abs(ThreadId),BytesRead); // add data received to received data string ReceivedData := ReceivedData + Buffer; // check to see if we have received the expected length yet if (ExpectedLength = 0) and (ReceivedData[1] = '{') and (Pos('}',ReceivedData) <> 0) then begin ExpectedLengthStr := Copy(ReceivedData,2,Pos('}', ReceivedData)-2); ExpectedLength := StrToIntDef(ExpectedLengthStr,0); Delete(ReceivedData,1,Pos('}', ReceivedData)); end; // check whether we have all the data we are expecting if (ExpectedLength > 0) and (Length(ReceivedData) >= ExpectedLength) then begin // only take the number of bytes expected and ignore the rest FServerCommunicator.AcquireGlobalDataLock; try ReceivedString := Encrypt(Copy(ReceivedData,1,ExpectedLength),ClientServerEncryptRandSeed); finally FServerCommunicator.ReleaseGlobalDataLock; end; // process data ProcessReceivedString; // reset ready for next time around ExpectedLength := 0; ReceivedData := ''; end; // just keep trying until we have it all StartTime := Now; end; // give other threads a chance if BytesRead = 0 then Sleep(1) else Sleep(0); // check termination flag if Terminated then Exit; end; finally Log('Thread cleanup'); if DatabaseLocked then FServerCommunicator.UnlockDatabase; // destroy the TWinSocketStream WinSocketStream.Free; // destroy the proxy database object collections for i := Low(ProxyDatabaseObjectCollection) to High(ProxyDatabaseObjectCollection) do ProxyDatabaseObjectCollection[i].Free; // close the database session FServerCommunicator.AcquireDatabaseLock; try if not Firebird then CloseSessionDatabase(TVSessionName) else begin TVFirebirdDatabase.Close; TVFirebirdDatabase.Free; end; finally FServerCommunicator.ReleaseDatabaseLock; end; // update the connection count FServerCommunicator.DecrementConnectionCount; // unregister thread information FServerCommunicator.FThreadInfoData.UnregisterThread(abs(ThreadId)); // unregister thread for messaging data FServerCommunicator.FMessagingData.UnregisterThread(abs(ThreadId)); // clear any remaining database object locks FServerCommunicator.FDatabaseObjectLocks.ClearAllLocks(UserName); end; end; end.