Commit 89533881 by Michael Brachmann

merge in main branch changes

parents 0efe52ee 3171db77
...@@ -1363,6 +1363,7 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -1363,6 +1363,7 @@ object ApiDatabaseModule: TApiDatabaseModule
Connection = ucENTCAD Connection = ucENTCAD
Events = 'disupdate' Events = 'disupdate'
OnEvent = UniAlerter1Event OnEvent = UniAlerter1Event
OnError = UniAlerter1Error
Left = 324 Left = 324
Top = 382 Top = 382
end end
......
...@@ -6,6 +6,7 @@ uses ...@@ -6,6 +6,7 @@ uses
System.SysUtils, System.Classes, Data.DB, MemDS, DBAccess, Uni, UniProvider, System.SysUtils, System.Classes, Data.DB, MemDS, DBAccess, Uni, UniProvider,
PostgreSQLUniProvider, System.Variants, System.Generics.Collections, System.IniFiles, PostgreSQLUniProvider, System.Variants, System.Generics.Collections, System.IniFiles,
Common.Logging, Vcl.Forms, System.Character, Common.Ini, DAAlerter, Common.Logging, Vcl.Forms, System.Character, Common.Ini, DAAlerter,
System.SyncObjs,
UniAlerter; UniAlerter;
type type
...@@ -210,14 +211,17 @@ type ...@@ -210,14 +211,17 @@ type
procedure DataModuleCreate(Sender: TObject); procedure DataModuleCreate(Sender: TObject);
procedure UniAlerter1Event(Sender: TDAAlerter; const EventName, procedure UniAlerter1Event(Sender: TDAAlerter; const EventName,
Message: string); Message: string);
procedure UniAlerter1Error(Sender: TDAAlerter; E: Exception);
private private
FCADUpdate: Integer;
{ Private declarations } FUpdateListenerFailed: Integer;
public public
CADUpdate: Boolean;
function HandleUniqueFilenames(const category: string): string; function HandleUniqueFilenames(const category: string): string;
function BadgeCounts(const BaseQuery: TUniQuery): Integer; function BadgeCounts(const BaseQuery: TUniQuery): Integer;
function EnsureConnected: Boolean; function EnsureConnected: Boolean;
function StartUpdateListener: Boolean;
procedure MarkCADUpdate;
function ConsumeCADUpdate: Boolean;
end; end;
var var
...@@ -232,7 +236,8 @@ implementation ...@@ -232,7 +236,8 @@ implementation
procedure TApiDatabaseModule.DataModuleCreate(Sender: TObject); procedure TApiDatabaseModule.DataModuleCreate(Sender: TObject);
begin begin
CADUpdate := False; FCADUpdate := 0;
FUpdateListenerFailed := 0;
ucENTCAD.ProviderName := 'PostgreSQL'; ucENTCAD.ProviderName := 'PostgreSQL';
ucENTCAD.Server := IniEntries.DatabaseServer; ucENTCAD.Server := IniEntries.DatabaseServer;
...@@ -241,8 +246,6 @@ begin ...@@ -241,8 +246,6 @@ begin
ucENTCAD.Username := IniEntries.DatabaseUsername; ucENTCAD.Username := IniEntries.DatabaseUsername;
ucENTCAD.Password := IniEntries.DatabasePassword; ucENTCAD.Password := IniEntries.DatabasePassword;
ucENTCAD.LoginPrompt := False; ucENTCAD.LoginPrompt := False;
EnsureConnected;
end; end;
...@@ -258,21 +261,64 @@ begin ...@@ -258,21 +261,64 @@ begin
ucENTCAD.ExecSQL('set search_path to lems, avl, entcad, public'); ucENTCAD.ExecSQL('set search_path to lems, avl, entcad, public');
Logger.Log(2, 'PostgreSQL API search_path set to lems, avl, entcad, public'); Logger.Log(2, 'PostgreSQL API search_path set to lems, avl, entcad, public');
end;
Result := True;
except
on E: Exception do
Logger.Log(1, 'PostgreSQL API database unavailable: ' + E.Message);
end;
end;
function TApiDatabaseModule.StartUpdateListener: Boolean;
begin
Result := False;
if TInterlocked.Exchange(FUpdateListenerFailed, 0) <> 0 then
begin
try
if UniAlerter1.Active then
UniAlerter1.Stop;
ucENTCAD.Disconnect;
except
on E: Exception do
begin
Logger.Log(1, 'PostgreSQL disupdate listener reset failed: ' + E.Message);
TInterlocked.Exchange(FUpdateListenerFailed, 1);
Exit;
end;
end;
end;
if not EnsureConnected then
Exit;
try
if not UniAlerter1.Active then
begin
Logger.Log(1, 'Starting PostgreSQL disupdate listener'); Logger.Log(1, 'Starting PostgreSQL disupdate listener');
UniAlerter1.Start; UniAlerter1.Start;
Logger.Log(1, 'PostgreSQL disupdate listener started'); Logger.Log(1, 'PostgreSQL disupdate listener started');
MarkCADUpdate;
CADUpdate := True;
end; end;
Result := True; Result := True;
except except
on E: Exception do on E: Exception do
Logger.Log(1, 'PostgreSQL API database unavailable: ' + E.Message); Logger.Log(1, 'PostgreSQL disupdate listener unavailable: ' + E.Message);
end; end;
end; end;
procedure TApiDatabaseModule.MarkCADUpdate;
begin
TInterlocked.Exchange(FCADUpdate, 1);
end;
function TApiDatabaseModule.ConsumeCADUpdate: Boolean;
begin
Result := TInterlocked.Exchange(FCADUpdate, 0) <> 0;
end;
procedure TApiDatabaseModule.uqComplaintListCalcFields(DataSet: TDataSet); procedure TApiDatabaseModule.uqComplaintListCalcFields(DataSet: TDataSet);
var var
raw: string; raw: string;
...@@ -343,7 +389,14 @@ end; ...@@ -343,7 +389,14 @@ end;
procedure TApiDatabaseModule.UniAlerter1Event(Sender: TDAAlerter; const EventName, Message: string); procedure TApiDatabaseModule.UniAlerter1Event(Sender: TDAAlerter; const EventName, Message: string);
begin begin
if SameText(EventName, 'disupdate') then if SameText(EventName, 'disupdate') then
CADUpdate := True; MarkCADUpdate;
end;
procedure TApiDatabaseModule.UniAlerter1Error(Sender: TDAAlerter; E: Exception);
begin
Logger.Log(1, 'PostgreSQL disupdate listener error: ' + E.Message);
TInterlocked.Exchange(FUpdateListenerFailed, 1);
MarkCADUpdate;
end; end;
function TApiDatabaseModule.BadgeCounts(const BaseQuery: TUniQuery): Integer; function TApiDatabaseModule.BadgeCounts(const BaseQuery: TUniQuery): Integer;
......
...@@ -59,7 +59,7 @@ begin ...@@ -59,7 +59,7 @@ begin
ApiDB := TApiDatabaseModule.Create(nil); ApiDB := TApiDatabaseModule.Create(nil);
if not ApiDB.ucENTCAD.Connected then if not ApiDB.EnsureConnected then
begin begin
Logger.Log(1, 'Unable to connect to API database'); Logger.Log(1, 'Unable to connect to API database');
raise EXDataHttpException.Create( raise EXDataHttpException.Create(
......
...@@ -13,8 +13,6 @@ type ...@@ -13,8 +13,6 @@ type
FAdminPassword: string; FAdminPassword: string;
FWebAppFolder: string; FWebAppFolder: string;
FReportsFolder: string; FReportsFolder: string;
FMemoLogLevel: Integer;
FFileLogLevel: Integer;
FAuditEnabled: Boolean; FAuditEnabled: Boolean;
FRpId: string; FRpId: string;
FRpName: string; FRpName: string;
...@@ -91,6 +89,15 @@ begin ...@@ -91,6 +89,15 @@ begin
end; end;
Logger.Log(1, '-- Config file found.'); Logger.Log(1, '-- Config file found.');
Logger.Log(1, '');
Logger.Log(1, '--- Server Config Values ---');
Logger.Log(1, '-- url: ' + serverConfig.url + IfThen(serverConfig.url = defaultServerUrl, ' [default]', ' [from config]'));
Logger.Log(1, '-- adminPassword: ' + serverConfig.adminPassword + IfThen(serverConfig.adminPassword = 'whatisthisusedfor', ' [default]', ' [from config]'));
Logger.Log(1, '-- jwtTokenSecret: ' + serverConfig.jwtTokenSecret + IfThen(serverConfig.jwtTokenSecret = 'super_secret0123super_secret4567', ' [default]', ' [from config]'));
Logger.Log(1, '-- webAppFolder: ' + serverConfig.webAppFolder + IfThen(serverConfig.webAppFolder = 'static', ' [default]', ' [from config]'));
Logger.Log(1, '-- auditEnabled: ' + BoolToStr(serverConfig.auditEnabled, True));
end
else
jsonObj := TJSONObject.ParseJSONValue(TFile.ReadAllText(configFile)) as TJSONObject; jsonObj := TJSONObject.ParseJSONValue(TFile.ReadAllText(configFile)) as TJSONObject;
if not Assigned(jsonObj) then if not Assigned(jsonObj) then
...@@ -143,8 +150,6 @@ begin ...@@ -143,8 +150,6 @@ begin
jwtTokenSecret := 'super_secret0123super_secret4567'; jwtTokenSecret := 'super_secret0123super_secret4567';
webAppFolder := 'static'; webAppFolder := 'static';
reportsFolder := 'reports'; reportsFolder := 'reports';
memoLogLevel := 3;
fileLogLevel := 4;
auditEnabled := False; auditEnabled := False;
rpId := 'wcemimobile.em-sys.net'; rpId := 'wcemimobile.em-sys.net';
rpName := 'emiMobile'; rpName := 'emiMobile';
......
...@@ -7,7 +7,7 @@ uses ...@@ -7,7 +7,7 @@ uses
System.SysUtils, System.SysUtils,
System.JSON, System.JSON,
System.Generics.Collections, System.Generics.Collections,
Vcl.ExtCtrls, System.SyncObjs,
VCL.TMSFNCWebSocketServer, VCL.TMSFNCWebSocketServer,
VCL.TMSFNCWebSocketCommon; VCL.TMSFNCWebSocketCommon;
...@@ -54,13 +54,15 @@ type ...@@ -54,13 +54,15 @@ type
FClients: TObjectList<TConnectedClient>; FClients: TObjectList<TConnectedClient>;
FClientsLock: TObject; FClientsLock: TObject;
FSendLock: TObject; FSendLock: TObject;
FHeartbeatTimer: TTimer; FHeartbeatThread: TThread;
FHeartbeatStopEvent: TEvent;
FStarted: Integer;
FOnClientsChanged: TClientsChangedEvent; FOnClientsChanged: TClientsChangedEvent;
function TrySendTextToClient(const AConnectionId, AMessage: string; function TrySendTextToClient(const AConnectionId, AMessage: string;
ALogFailure: Boolean = True): Boolean; ALogFailure: Boolean = True): Boolean;
function TryCloseClient(const AConnectionId: string): Boolean; function TryCloseClient(const AConnectionId: string): Boolean;
procedure HeartbeatTimer(Sender: TObject); procedure HeartbeatSweep;
procedure NotifyClientsChanged; procedure NotifyClientsChanged;
procedure HandshakeResponseSent(Sender: TObject; AConnection: TTMSFNCWebSocketServerConnection); procedure HandshakeResponseSent(Sender: TObject; AConnection: TTMSFNCWebSocketServerConnection);
procedure MessageReceived(Sender: TObject; AConnection: TTMSFNCWebSocketConnection; const AMessage: string); procedure MessageReceived(Sender: TObject; AConnection: TTMSFNCWebSocketConnection; const AMessage: string);
...@@ -104,17 +106,32 @@ begin ...@@ -104,17 +106,32 @@ begin
FServer.OnMessageReceived := MessageReceived; FServer.OnMessageReceived := MessageReceived;
FServer.OnDisconnect := ClientDisconnected; FServer.OnDisconnect := ClientDisconnected;
FHeartbeatTimer := TTimer.Create(nil); FStarted := 0;
FHeartbeatTimer.Enabled := False; FHeartbeatStopEvent := TEvent.Create(nil, False, False, '');
FHeartbeatTimer.Interval := HEARTBEAT_SWEEP_INTERVAL_MS; FHeartbeatThread := TThread.CreateAnonymousThread(
FHeartbeatTimer.OnTimer := HeartbeatTimer; procedure
begin
while not TThread.CurrentThread.CheckTerminated do
begin
if FHeartbeatStopEvent.WaitFor(HEARTBEAT_SWEEP_INTERVAL_MS) = wrSignaled then
Break;
if TInterlocked.CompareExchange(FStarted, 0, 0) <> 0 then
HeartbeatSweep;
end;
end);
FHeartbeatThread.FreeOnTerminate := False;
FHeartbeatThread.Start;
end; end;
destructor TWebSocketManager.Destroy; destructor TWebSocketManager.Destroy;
begin begin
FHeartbeatTimer.Enabled := False;
Stop; Stop;
FHeartbeatTimer.Free; FHeartbeatThread.Terminate;
FHeartbeatStopEvent.SetEvent;
FHeartbeatThread.WaitFor;
FHeartbeatThread.Free;
FHeartbeatStopEvent.Free;
FServer.Free; FServer.Free;
FClients.Free; FClients.Free;
FSendLock.Free; FSendLock.Free;
...@@ -126,14 +143,14 @@ end; ...@@ -126,14 +143,14 @@ end;
procedure TWebSocketManager.Start; procedure TWebSocketManager.Start;
begin begin
FServer.Active := True; FServer.Active := True;
FHeartbeatTimer.Enabled := True; TInterlocked.Exchange(FStarted, 1);
end; end;
procedure TWebSocketManager.Stop; procedure TWebSocketManager.Stop;
begin begin
if Assigned(FHeartbeatTimer) then TInterlocked.Exchange(FStarted, 0);
FHeartbeatTimer.Enabled := False; if Assigned(FServer) then
FServer.Active := False; FServer.Active := False;
end; end;
function TWebSocketManager.TrySendTextToClient(const AConnectionId, function TWebSocketManager.TrySendTextToClient(const AConnectionId,
...@@ -141,9 +158,11 @@ function TWebSocketManager.TrySendTextToClient(const AConnectionId, ...@@ -141,9 +158,11 @@ function TWebSocketManager.TrySendTextToClient(const AConnectionId,
var var
client: TConnectedClient; client: TConnectedClient;
connection: TTMSFNCWebSocketServerConnection; connection: TTMSFNCWebSocketServerConnection;
sendFailed: Boolean;
begin begin
Result := False; Result := False;
connection := nil; connection := nil;
sendFailed := False;
// TMS owns and frees the connection immediately after its disconnect // TMS owns and frees the connection immediately after its disconnect
// callback returns. Holding FSendLock makes that callback wait until the // callback returns. Holding FSendLock makes that callback wait until the
...@@ -174,13 +193,35 @@ begin ...@@ -174,13 +193,35 @@ begin
except except
on E: Exception do on E: Exception do
begin begin
sendFailed := True;
if ALogFailure then if ALogFailure then
Logger.Log(2, 'WebSocket send failed: ' + E.Message); Logger.Log(2, 'WebSocket send failed for ' + AConnectionId + ': ' + E.Message);
end;
end;
if sendFailed then
begin
TMonitor.Enter(FClientsLock);
try
for client in FClients do
if SameText(client.ConnectionId, AConnectionId) then
begin
client.Closing := True;
Break;
end;
finally
TMonitor.Exit(FClientsLock);
end; end;
end; end;
finally finally
TMonitor.Exit(FSendLock); TMonitor.Exit(FSendLock);
end; end;
if sendFailed then
begin
NotifyClientsChanged;
TryCloseClient(AConnectionId);
end;
end; end;
function TWebSocketManager.TryCloseClient( function TWebSocketManager.TryCloseClient(
...@@ -224,7 +265,7 @@ begin ...@@ -224,7 +265,7 @@ begin
end; end;
end; end;
procedure TWebSocketManager.HeartbeatTimer(Sender: TObject); procedure TWebSocketManager.HeartbeatSweep;
var var
staleConnectionIds: TList<string>; staleConnectionIds: TList<string>;
client: TConnectedClient; client: TConnectedClient;
......
unit Ws.DataModel; unit Ws.DataModel;
// Server-side WebSocket data model. // Server-side WebSocket data model.
// Owns a VCL timer that fires every FIntervalMs milliseconds, queries the // Owns a worker thread that checks every FIntervalMs milliseconds, queries the
// database for the five data sets used by the polling timers that existed in // database for the five data sets used by the polling timers that existed in
// each connected browser client, and broadcasts the results to every // each connected browser client, and broadcasts the results to every
// handshaked WebSocket connection via the supplied broadcast callback. // handshaked WebSocket connection via the supplied broadcast callback.
...@@ -14,9 +14,8 @@ interface ...@@ -14,9 +14,8 @@ interface
uses uses
System.SysUtils, System.Classes, System.JSON, System.SysUtils, System.Classes, System.JSON,
System.Generics.Collections, System.Generics.Collections, System.SyncObjs,
Data.DB, Data.DB,
Vcl.ExtCtrls,
Api.Database, Api.Database,
WsMessages, WsMessages,
Common.Logging; Common.Logging;
...@@ -27,10 +26,13 @@ type ...@@ -27,10 +26,13 @@ type
TWsDataModel = class TWsDataModel = class
private private
FDb: TApiDatabaseModule; FDb: TApiDatabaseModule;
FTimer: TTimer; FThread: TThread;
FStopEvent: TEvent;
FIntervalMs: Integer;
FBroadcast: TWsBroadcastProc; FBroadcast: TWsBroadcastProc;
procedure TimerFire(Sender: TObject); procedure Execute;
procedure ConfigureQueryTimeouts;
procedure BroadcastAll; procedure BroadcastAll;
function BuildBadgeCountsJson: string; function BuildBadgeCountsJson: string;
...@@ -48,37 +50,84 @@ implementation ...@@ -48,37 +50,84 @@ implementation
uses uses
System.StrUtils, System.DateUtils; System.StrUtils, System.DateUtils;
const
UNIT_MAP_REFRESH_INTERVAL_MS = 15000;
{ TWsDataModel } { TWsDataModel }
constructor TWsDataModel.Create(ABroadcast: TWsBroadcastProc; AIntervalMs: Integer); constructor TWsDataModel.Create(ABroadcast: TWsBroadcastProc; AIntervalMs: Integer);
begin begin
inherited Create; inherited Create;
FBroadcast := ABroadcast; FBroadcast := ABroadcast;
FIntervalMs := AIntervalMs;
FStopEvent := TEvent.Create(nil, False, False, '');
FDb := TApiDatabaseModule.Create(nil); FDb := TApiDatabaseModule.Create(nil);
FTimer := TTimer.Create(nil); ConfigureQueryTimeouts;
FTimer.Interval := AIntervalMs; FThread := TThread.CreateAnonymousThread(Execute);
FTimer.OnTimer := TimerFire; FThread.FreeOnTerminate := False;
FTimer.Enabled := True; FThread.Start;
end; end;
destructor TWsDataModel.Destroy; destructor TWsDataModel.Destroy;
begin begin
FTimer.Enabled := False; FThread.Terminate;
FTimer.Free; FStopEvent.SetEvent;
FThread.WaitFor;
FThread.Free;
FDb.Free; FDb.Free;
FStopEvent.Free;
inherited; inherited;
end; end;
procedure TWsDataModel.TimerFire(Sender: TObject); procedure TWsDataModel.Execute;
var
unitMapElapsedMs: Integer;
begin begin
if not FDb.ucENTCAD.Connected then unitMapElapsedMs := 0;
Exit;
try
while not TThread.CurrentThread.CheckTerminated do
begin
if FStopEvent.WaitFor(FIntervalMs) = wrSignaled then
Break;
Inc(unitMapElapsedMs, FIntervalMs);
if not FDb.CADUpdate then if not FDb.StartUpdateListener then
Exit; Continue;
FDb.CADUpdate := False; if FDb.ConsumeCADUpdate then
BroadcastAll; begin
BroadcastAll;
unitMapElapsedMs := 0;
end
else if unitMapElapsedMs >= UNIT_MAP_REFRESH_INTERVAL_MS then
begin
try
FBroadcast(BuildUnitMapJson);
except
on E: Exception do
Logger.Log(2, 'WsDataModel UNIT_MAP refresh error: ' + E.Message);
end;
unitMapElapsedMs := 0;
end;
end;
except
on E: Exception do
Logger.Log(1, 'WsDataModel worker stopped: ' + E.Message);
end;
end;
procedure TWsDataModel.ConfigureQueryTimeouts;
const
COMMAND_TIMEOUT_SECONDS = 15;
begin
FDb.uqBadgeCounts.SpecificOptions.Values['CommandTimeout'] := IntToStr(COMMAND_TIMEOUT_SECONDS);
FDb.uqMapUnits.SpecificOptions.Values['CommandTimeout'] := IntToStr(COMMAND_TIMEOUT_SECONDS);
FDb.uqMapComplaintUnitsList.SpecificOptions.Values['CommandTimeout'] := IntToStr(COMMAND_TIMEOUT_SECONDS);
FDb.uqMapComplaints.SpecificOptions.Values['CommandTimeout'] := IntToStr(COMMAND_TIMEOUT_SECONDS);
FDb.uqUnitList.SpecificOptions.Values['CommandTimeout'] := IntToStr(COMMAND_TIMEOUT_SECONDS);
FDb.uqComplaintList.SpecificOptions.Values['CommandTimeout'] := IntToStr(COMMAND_TIMEOUT_SECONDS);
end; end;
procedure TWsDataModel.BroadcastAll; procedure TWsDataModel.BroadcastAll;
......
[Settings] [Settings]
LogFileNum=107 LogFileNum=109
webClientVersion=0.9.4.3 webClientVersion=0.9.4.3
[Database] [Database]
......
...@@ -2,7 +2,5 @@ ...@@ -2,7 +2,5 @@
"url": "http://localhost:2001/emsys/emiMobile/", "url": "http://localhost:2001/emsys/emiMobile/",
"jwtTokenSecret": "super_secret0123super_secret4567", "jwtTokenSecret": "super_secret0123super_secret4567",
"adminPassword": "whatisthisusedfor?", "adminPassword": "whatisthisusedfor?",
"webAppFolder": "static", "webAppFolder": "static"
"memoLogLevel": 5, }
"fileLogLevel": 5
}
\ No newline at end of file
...@@ -2,6 +2,7 @@ program emiMobileServer; ...@@ -2,6 +2,7 @@ program emiMobileServer;
uses uses
FastMM4, FastMM4,
System.Classes,
System.SyncObjs, System.SyncObjs,
System.SysUtils, System.SysUtils,
Vcl.StdCtrls, Vcl.StdCtrls,
...@@ -81,6 +82,9 @@ var ...@@ -81,6 +82,9 @@ var
LogMsg: string; LogMsg: string;
begin begin
if logLevel > FLogLevel then
Exit;
FCriticalSection.Acquire; FCriticalSection.Acquire;
try try
LogTime := Now; LogTime := Now;
...@@ -92,11 +96,16 @@ begin ...@@ -92,11 +96,16 @@ begin
else else
FormattedMessage := FormattedMessage + '[' + IntToStr(logLevel) +'] ' + LogMsg; FormattedMessage := FormattedMessage + '[' + IntToStr(logLevel) +'] ' + LogMsg;
if logLevel <= FLogLevel then
FLogMemo.Lines.Add( FormattedMessage );
finally finally
FCriticalSection.Release; FCriticalSection.Release;
end; end;
TThread.Queue(nil,
procedure
begin
if Assigned(FLogMemo) and not (csDestroying in FLogMemo.ComponentState) then
FLogMemo.Lines.Add(FormattedMessage);
end);
end; end;
{ TFileLogAppender } { TFileLogAppender }
......
...@@ -825,7 +825,8 @@ begin ...@@ -825,7 +825,8 @@ begin
FHeartbeatOutstanding := False; FHeartbeatOutstanding := False;
CancelHeartbeatTimeout; CancelHeartbeatTimeout;
ScheduleHeartbeat(HEARTBEAT_INTERVAL_MS); if FHeartbeatTimerId = 0 then
ScheduleHeartbeat(HEARTBEAT_INTERVAL_MS);
end; end;
procedure TdmWebsocket.RegisterLifecycleListeners; procedure TdmWebsocket.RegisterLifecycleListeners;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment