Synchronise strange behavior in service - multithreading

I have a service where in the main thread I store some data and read it sometimes from the child thread.
With Delphi 7 everything worked fine.
Service execute, child thread create, main thread made the data, child thread called Synchronise to get it ... and waited until main thread ServiceThread.ProcessRequests(True);
Now with Delphi 10.3 it seems that Synchronise is not waiting for the main thread to get to the ProcessRequests (idle) ... it calls in the middle of the main Execute processing.
main service thread:
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.SvcMgr, Vcl.Dialogs;
type
TTestserv2 = class(TService)
procedure ServiceExecute(Sender: TService);
private
{ Private declarations }
procedure log(msg: String);
public
function GetServiceController: TServiceController; override;
function getArrayItem(i: integer): string;
{ Public declarations }
protected
function DoCustomControl(CtrlCode: Cardinal): Boolean; override;
end;
Const
SERVICE_CONTROL_MyMSG = 10;
var
Testserv2: TTestserv2;
implementation
{$R *.dfm}
Uses unit2;
Var
array1 : Array of string;
Thread1 : T_Thread1;
procedure ServiceController(CtrlCode: DWord); stdcall;
begin
Testserv2.Controller(CtrlCode);
end;
function TTestserv2.GetServiceController: TServiceController;
begin
Result := ServiceController;
end;
procedure TTestserv2.log(msg: String);
Var
F:TextFile;
LogFile:String;
TmpStr:String;
begin
try
LogFile := 'c:\testlog1.txt';
AssignFile(F, LogFile);
If FileExists(LogFile) then
Append(F)
Else
Rewrite(F);
DateTimeToString(TmpStr,'yyyy.mm.dd. hh:nn:ss',now);
WriteLN(F,TmpStr+' - '+Msg);
Flush(F);
Finally
CloseFile(F);
End;
end;
function TTestserv2.DoCustomControl(CtrlCode: Cardinal): Boolean;
begin
result := true;
case CtrlCode of
SERVICE_CONTROL_MyMSG : log('MyMSG');
end;
end;
procedure TTestserv2.ServiceExecute(Sender: TService);
var
Msg: String;
i: integer;
s: string;
Begin
Log('Service Execute');
SetLength(array1, 20);
Thread1 := T_Thread1.Create;
Thread1.Priority:=tpNormal;
Thread1.Resume;
Log('Thread1 created');
// Where the magic happens
for i := 0 to 21 do
Begin
s := 'value='+ IntToStr( i*2);
array1[i] := s;
Log( IntToStr(i) + '-' + s);
sleep(100); // in real code some idSNMP query here
End;
while not Terminated do
begin
Sleep(50);
Log('Service Execute OK ');
If Terminated then
Log('Terminated');
ServiceThread.ProcessRequests(True);
end;
End;
function TTestserv2.getArrayItem(i:integer):string;
Begin
result := array1[i];
End;
end.
Child thread:
unit unit2;
interface
uses
Windows, Classes, SysUtils, ExtCtrls, SyncObjs, ADODB, ActiveX, Unit1;
type
T_Thread1 = class(TThread)
private
{ Private declarations }
FWakeupEvent : TSimpleEvent;
procedure Log(Msg:String);
procedure Terminate1(Sender: TObject);
Procedure getdataproc;
protected
procedure Execute; override;
public
constructor Create;
Destructor Destroy; override;
end;
implementation
{ T_Thread1 }
constructor T_Thread1.Create;
begin
inherited Create(True);
OnTerminate := Terminate1;
FreeOnTerminate := False;
End;
procedure T_Thread1.Terminate1(Sender: TObject);
Var
s2:String;
begin
CoUninitialize;
End;
Destructor T_Thread1.Destroy;
Begin
If not Terminated Then Terminate;
inherited;
End;
procedure T_Thread1.log(msg: String);
Var
F:TextFile;
LogFile:String;
TmpStr:String;
begin
try
LogFile := 'c:\testlog2.txt';
AssignFile(F, LogFile);
If FileExists(LogFile) then
Append(F)
Else
Rewrite(F);
DateTimeToString(TmpStr,'hh:nn:ss',now);
WriteLN(F,TmpStr+' - '+Msg);
Flush(F);
Finally
CloseFile(F);
End;
end;
procedure T_Thread1.Execute;
Var
WaitStatus: Cardinal;
begin
LOG('Execute Start');
CoInitialize(nil);
FWakeupEvent := TSimpleEvent.Create;
repeat
WaitStatus := WaitForSingleObject(FWakeupEvent.Handle, 1000);
case WaitStatus of
WAIT_OBJECT_0: Break;
WAIT_TIMEOUT:
Begin
Log('Timeout');
Synchronize(getdataproc);
End;
Else Break;
end;
until (Terminated);
FreeAndNil(FWakeupEvent);
end;
Procedure T_Thread1.getdataproc;
Var
i:integer;
res:string;
Begin
for i := 0 to 21 do
Begin
res := Testserv2.getArrayItem(i);
log(IntToStr(i)+ '-' + res);
End;
End;
end.
and the result
log1 for main:
16:27:01 - Service Execute
16:27:01 - Thread1 created
16:27:01 - 0-value=0
16:27:01 - 1-value=2
16:27:01 - 2-value=4
16:27:01 - 3-value=6
16:27:01 - 4-value=8
16:27:01 - 5-value=10
16:27:01 - 6-value=12
16:27:02 - 7-value=14
16:27:02 - 8-value=16
16:27:02 - 9-value=18
16:27:02 - 10-value=20
16:27:02 - 11-value=22
16:27:02 - 12-value=24
16:27:02 - 13-value=26
16:27:02 - 14-value=28
16:27:02 - 15-value=30
16:27:03 - 16-value=32
16:27:03 - 17-value=34
16:27:03 - 18-value=36
16:27:03 - 19-value=38
16:27:03 - 20-value=40
16:27:03 - 21-value=42
16:27:03 - Service Execute OK
log2 for child thread:
16:27:01 - Execute Start
16:27:02 - Timeout
16:27:02 - 0-value=0
16:27:02 - 1-value=2
16:27:02 - 2-value=4
16:27:02 - 3-value=6
16:27:02 - 4-value=8
16:27:02 - 5-value=10
16:27:02 - 6-value=12
16:27:02 - 7-value=14
16:27:02 - 8-value=16
16:27:02 - 9-value=18
16:27:02 - 10-
16:27:02 - 11-
16:27:02 - 12-
16:27:02 - 13-
16:27:02 - 14-
16:27:02 - 15-
16:27:02 - 16-
16:27:02 - 17-
16:27:02 - 18-
16:27:02 - 19-
16:27:02 - 20-
16:27:02 - 21-
16:27:03 - Timeout
16:27:03 - 0-value=0
16:27:03 - 1-value=2
16:27:03 - 2-value=4
16:27:03 - 3-value=6
16:27:03 - 4-value=8
16:27:03 - 5-value=10
16:27:03 - 6-value=12
16:27:03 - 7-value=14
16:27:03 - 8-value=16
16:27:03 - 9-value=18
16:27:03 - 10-value=20
16:27:03 - 11-value=22
16:27:03 - 12-value=24
16:27:03 - 13-value=26
16:27:03 - 14-value=28
16:27:03 - 15-value=30
16:27:03 - 16-value=32
16:27:03 - 17-value=34
16:27:03 - 18-value=36
16:27:03 - 19-
16:27:03 - 20-
16:27:03 - 21-
16:27:04 - Timeout
16:27:04 - 0-value=0
16:27:04 - 1-value=2
16:27:04 - 2-value=4
16:27:04 - 3-value=6
16:27:04 - 4-value=8
16:27:04 - 5-value=10
16:27:04 - 6-value=12
16:27:04 - 7-value=14
16:27:04 - 8-value=16
16:27:04 - 9-value=18
16:27:04 - 10-value=20
16:27:04 - 11-value=22
16:27:04 - 12-value=24
16:27:04 - 13-value=26
16:27:04 - 14-value=28
16:27:04 - 15-value=30
16:27:04 - 16-value=32
16:27:04 - 17-value=34
16:27:04 - 18-value=36
16:27:04 - 19-value=38
16:27:04 - 20-value=40
16:27:04 - 21-value=42
So for the first two round the child calls in the middle of the for cycle of the main.
Does not wait. In real code array is an array of records with more string and integer items.
Sometimes (very very rare) result is like this: ???†??????e se OK ?ô
Like Synchronise is not working properly. (compiled to 32 and 64 bit, same result)
What can I do? Not thrust Synchronise ? Criticalsection ?
Do not want to rewrite everything.
The child PostThreadMessage CM_SERVICE_CONTROL_CODE to main, and main PostThreadMessage back with a bit more data (some kB) ... I try to avoid.
Any suggestions ?

The TService.OnExecute event is NOT fired in the actual main thread! It is fired in a worker thread that is created by the main thread. The main message loop that handles TThread.Synchronize() requests is in the project's .dpr file where TServiceApplication.Run() is called.
In a typical TService project, there are at least 3 threads running by default:
the project main thread, which handles the main message loop, and fires each TService's (Before|After)Install and (Before|After)Uninstall events if needed.
the StartServiceCtrlDispatcher() thread, which maintains a connection to the SCM, and dispatches SCM requests to each TService.Controller callback.
a thread for each TService, which fires that service's On(Start|Stop|Shutdown), On(Pause|Continue), and OnExecute events based on SCM requests received by the StartServiceCtrlDispatcher() thread.
When your OnExecute event handler calls ServiceThread.ProcessRequests(), it is handling pending SCM requests - in the form of CM_SERVICE_CONTROL_CODE messages that are posted to the TService's thread from the TService.Controller callback function, which is called by StartServiceCtrlDispatcher() in a worker thread that is created by the main thread. It is NOT handling pending Synchronize() requests at all.
So, your 2 threads are NOT synchronizing with each other at all. You need to re-think your synchronization logic. If you want your T_Thread1 to sync with your TTestserv2, then one option would be to have TTestserv2 create a hidden HWND for itself (such as with System.Classes.AllocateHWnd()) and then T_Thread1 can send/post window messages to that HWND as needed. Calling ProcessRequests() in the OnExecute event (in TTestserv2's thread) will dispatch those window messages as needed.
Also, speaking of ProcessRequests(), know that calling ProcessRequests() with WaitForMessage=True will block the calling thread until the service is terminated, processing all SCM requests (and window messages) internally as needed. If you want your OnExecute event handler to run its own loop, you need to call ProcessRequests() with WaitForMessage=False instead.
And FYI, everything I have said applies to Delphi 7, too.

Related

Delphi ADO EDatabaseError connecting to access database from different threads

I am attempting to open a connection to an Access database using a TADOConnection created on separate threads. On opening the connection an EDatabaseError 'External exception C06D007F' is thrown in Data.Win.ADODB. I tried searching and was only able to find examples adding calls to CoInitialize/CoUninitialize, and am not sure what else is wrong with my code. The same connection code succeeds when not executed in a TTask or TThread.
Update: The code works when I compile the test app with 32 bit, but fails with 64 bit. I have both Access 32 bit and 64 bit database engines installed on my computer. I uninstalled the 64 bit engine and reinstalled with the latest one from Microsoft and the errors went away.
const
ConnectionString: string = 'Provider=Microsoft.ACE.OLEDB.16.0;Data Source="C:\Users\Public\Documents\TestDatabase.mdb"';
function TMainForm.CreateADOConnection(const AConnectionString: string): TADOConnection;
begin
Result := TADOConnection.Create(nil);
Result.ConnectionString := AConnectionString;
Result.LoginPrompt := False;
end;
procedure TMainForm.CreateDBConnections;
var
LTasks: array of ITask;
begin
SetLength(LTasks, 2);
LTasks[0] := TTask.Create(procedure
var
LConn1: TADOConnection;
begin
CoInitialize(nil);
try
LConn1 := CreateADOConnection(ConnectionString);
try
LConn1.Open;
LConn1.Close;
finally
LConn1.Free;
end;
finally
CoUninitialize;
end;
end);
LTasks[1] := TTask.Create(
procedure
var
LConn2: TADOConnection;
begin
CoInitialize(nil);
try
LConn2 := CreateADOConnection(ConnectionString);
try
LConn2.Open;
LConn2.Close;
finally
LConn2.Free;
end;
finally
CoUninitialize;
end;
end);
LTasks[0].Start;
LTasks[1].Start;
TTask.WaitForAll(LTasks);
end;

Inno Setup - Progress bar doesn't show when uninstall

I'm using Inno Setup to create my own installer. When user uninstall app I want delete some folder.
So I use CurUninstallStepChanged event to delete folder and show "progress bar" with npbstMarquee style (based on Inno Setup: How to handle progress bar on [UninstallDelete] section?).
Here is the code:
procedure DeleteFolder();
var
FindRec: TFindRec;
fullPath: string;
tmpMsg: string;
StatusText: string;
deletePath: string;
begin
{ find all and delete }
UninstallProgressForm.ProgressBar.Style := npbstMarquee;
StatusText := UninstallProgressForm.StatusLabel.Caption;
UninstallProgressForm.StatusLabel.WordWrap := True;
UninstallProgressForm.StatusLabel.AutoSize := True;
fullPath := 'C:\ProgramData\TestPath';
if FindFirst(ExpandConstant(fullPath + '\*'), FindRec) then
try
repeat
if (FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY <> 0) and
(FindRec.Name <> '.') and (FindRec.Name <> '..') then begin
deletePath := AddBackslash(fullPath) + FindRec.Name;
tmpMsg := 'Deleting...' + #13#10 + deletePath;
UninstallProgressForm.StatusLabel.Caption := tmpMsg;
DelTree(deletePath, True, True, True);
end;
until
not FindNext(FindRec);
finally
UninstallProgressForm.StatusLabel.Caption := StatusText;
FindClose(FindRec);
end;
UninstallProgressForm.ProgressBar.Style := npbstNormal;
end;
{ Uninstall event }
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
begin
case CurUninstallStep of
usUninstall:
begin
DeleteFolder();
end;
end;
end;
If I using debug each line, I can see progress bar running. But when I using unins000.exe then only Caption can show, progress bar is not showing.
How can I fix it?
You have to pump the message queue to display/animate the progress bar.
Inno Setup: How to modify long running script so it will not freeze GUI?
Particularly, you can use the AppProcessMessage function from:
My SQL server discovery on LAN by listening port (Inno Setup)
Though with use of DelTree, the interval between calls to AppProcessMessage will be too big to animate the progress bar smoothly. You would have to implement a recursive delete explicitly to allow pumping the queue frequently enough.

I want to write a log file instead of message box in inno setup

I tried to use the following code but the log is not writing
procedure CurStepChanged(CurStep: TSetupStep);
var
logfilepathname, logfilename, newfilepathname: string;
begin
logfilepathname := ExpandConstant('{log}');
logfilename := ExtractFileName(logfilepathname);
newfilepathname := ExpandConstant('C:\Spectrum\StaticFilesLog\') + logfilename;
if CurStep = ssDone then
begin
FileCopy(logfilepathname, newfilepathname, false);
end;
end;
function SQLServerInstallation: Boolean;
begin
if (IsSQlServer2012Present = True) then
begin
Result := True;
end
else if (IsSQlServer2005Present = True) then
begin
Log('File : ' + 'Static file installation'); // this is not working
Log('SQL server 2005 is present in your machine. Please install SQL');
// It should write the log but not writing
ExitProcess(0);
end
else
begin
Log('File : ' + 'Static file installation'); // This is not working
Log('SQL server was not installed in your machine please install 2005')
// It should write the log but not writing
ExitProcess(0);
end;
end;

Goes to a site when setup is closed / finished / uninstalled

Inno Setup Compiler: How to auto start the default browser with given url? is not what I am wanting for. How / what code in Inno Setup when I want my setup.exe that if it's closed / finished / uninstalled, it will go to a certain site.
To open a web browser from a Pascal Script use a function like this:
procedure OpenBrowser(Url: string);
var
ErrorCode: Integer;
begin
ShellExec('open', Url, '', '', SW_SHOWNORMAL, ewNoWait, ErrorCode);
end;
To trigger it, when installer closes, you can use the CurStepChanged(ssDone) event:
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssDone then
begin
OpenBrowser('https://www.example.com/');
end;
end;
Similarly for an uninstaller, use the CurUninstallStepChanged(usDone) event:
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
begin
if CurUninstallStep = usDone then
begin
OpenBrowser('https://www.example.com/');
end;
end;

Global, thread safe, cookies manager with Indy

My Delphi 2010 app uploads stuff using multi-threading, uploaded data is POSTed to a PHP/web application which requires login, so I need to use a shared/global cookies manager (I'm using Indy10 Revision 4743) since TIdCookieManager is not thread-safe :(
Also, server side, session id is automatically re-generated every 5 minutes, so I must keep both the global & local cookie managers in sync.
My code looks like this:
TUploadThread = class(TThread)
// ...
var
GlobalCookieManager : TIdCookieManager;
procedure TUploadThread.Upload(FileName : String);
var
IdHTTP : TIdHTTP;
TheSSL : TIdSSLIOHandlerSocketOpenSSL;
TheCompressor : TIdCompressorZLib;
TheCookieManager : TIdCookieManager;
AStream : TIdMultipartFormDataStream;
begin
ACookieManager := TIdCookieManager.Create(IdHTTP);
// Automatically sync cookies between local & global Cookie managers
#TheCookieManager.OnNewCookie := pPointer(Cardinal(pPointer( procedure(ASender : TObject; ACookie : TIdCookie; var VAccept : Boolean)
begin
OmniLock.Acquire;
try
GlobalCookieManager.CookieCollection.AddCookie(ACookie, TIdHTTP(TIdCookieManager(ASender).Owner).URL{IdHTTP.URL});
finally
OmniLock.Release;
end; // try/finally
VAccept := True;
end )^ ) + $0C)^;
// ======================================== //
IdHTTP := TIdHTTP.Create(nil);
with IdHTTP do
begin
HTTPOptions := [hoForceEncodeParams, hoNoParseMetaHTTPEquiv];
AllowCookies := True;
HandleRedirects := True;
ProtocolVersion := pv1_1;
IOHandler := TheSSL;
Compressor := TheCompressor;
CookieManager := TheCookieManager;
end; // with
OmniLock.Acquire;
try
// Load login info/cookies
TheCookieManager.CookieCollection.AddCookies(GlobalCookieManager.CookieCollection);
finally
OmniLock.Release;
end; // try/finally
AStream := TIdMultipartFormDataStream.Create;
with Stream.AddFile('file_name', FileName, 'application/octet-stream') do
begin
HeaderCharset := 'utf-8';
HeaderEncoding := '8';
end; // with
IdHTTP.Post('https://www.domain.com/post.php', AStream);
AStream.Free;
end;
But it doesn't work! I'm getting this exception when calling AddCookies()
Project MyEXE.exe raised exception class EAccessViolation with message
'Access violation at address 00000000. Read of address 00000000'.
I also tried using assign(), ie.
TheCookieManager.CookieCollection.Assign(GlobalCookieManager.CookieCollection);
But I still get the same exception, usually here:
TIdCookieManager.GenerateClientCookies()
Anyone knows how to fix this?
Don't use an anonymous procedure for the OnNewCookie event. Use a normal class method instead:
procedure TUploadThread.NewCookie(ASender: TObject; ACookie : TIdCookie; var VAccept : Boolean);
var
LCookie: TIdCookie;
begin
LCookie := TIdCookieClass(ACookie.ClassType).Create;
LCookie.Assign(ACookie);
OmniLock.Acquire;
try
GlobalCookieManager.CookieCollection.AddCookie(LCookie, TIdHTTP(TIdCookieManager(ASender).Owner).URL);
finally
OmniLock.Release;
end;
VAccept := True;
end;
Or:
procedure TUploadThread.NewCookie(ASender: TObject; ACookie : TIdCookie; var VAccept : Boolean);
begin
OmniLock.Acquire;
try
GlobalCookieManager.CookieCollection.AddServerCookie(ACookie.ServerCookie, TIdHTTP(TIdCookieManager(ASender).Owner).URL);
finally
OmniLock.Release;
end;
VAccept := True;
end;
Then use it like this:
procedure TUploadThread.Upload(FileName : String);
var
IdHTTP : TIdHTTP;
TheSSL : TIdSSLIOHandlerSocketOpenSSL;
TheCompressor : TIdCompressorZLib;
TheCookieManager : TIdCookieManager;
TheStream : TIdMultipartFormDataStream;
begin
IdHTTP := TIdHTTP.Create(nil);
try
...
TheCookieManager := TIdCookieManager.Create(IdHTTP);
TheCookieManager.OnNewCookie := NewCookie;
with IdHTTP do
begin
HTTPOptions := [hoForceEncodeParams, hoNoParseMetaHTTPEquiv];
AllowCookies := True;
HandleRedirects := True;
ProtocolVersion := pv1_1;
IOHandler := TheSSL;
Compressor := TheCompressor;
CookieManager := TheCookieManager;
end; // with
OmniLock.Acquire;
try
// Load login info/cookies
TheCookieManager.CookieCollection.AddCookies(GlobalCookieManager.CookieCollection);
finally
OmniLock.Release;
end;
TheStream := TIdMultipartFormDataStream.Create;
try
with TheStream.AddFile('file_name', FileName, 'application/octet-stream') do
begin
HeaderCharset := 'utf-8';
HeaderEncoding := '8';
end;
IdHTTP.Post('https://www.domain.com/post.php', TheStream);
finally
TheStream.Free;
end;
finally
IdHTTP.Free;
end;
end;
If I had to guess, I'd say your problem is in here somewhere:
// Automatically sync cookies between local & global Cookie managers
#TheCookieManager.OnNewCookie := pPointer(Cardinal(pPointer( procedure(ASender : TObject; ACookie : TIdCookie; var VAccept : Boolean)
begin
OmniLock.Acquire;
try
GlobalCookieManager.CookieCollection.AddCookie(ACookie, TIdHTTP(TIdCookieManager(ASender).Owner).URL{IdHTTP.URL});
finally
OmniLock.Release;
end; // try/finally
VAccept := True;
end )^ ) + $0C)^;
I'm not sure what the $0C magic number is there for, but I bet all those casts are there because you had a heck of a time getting the compiler to accept this. It gave you type errors saying you couldn't assign the one thing to the other.
Those type errors are there for a reason! If you hack your way around the type system, things are very likely to break. Try turning that anonymous method into a normal method on TUploadThread and assign it that way, and see if it doesn't work better.
Responding to the comment:
Thank you guys, I converted to a normal method, but I'm still getting
exceptions in AddCookies(), last one happened in the line that reads
FRWLock.BeginWrite; in this procedure
TIdCookies.LockCookieList(AAccessType: TIdCookieAccess):
TIdCookieList;
If your error is an Access Violation with Read of address 00000000, that's got a very specific meaning. It means you're trying to do something with an object that's nil.
When you get that, break to the debugger. If the error's taking place on the line you said it's happening on, then it's almost certain that either Self or FRWLock is nil at this point. Check both variables and figure out which one hasn't been constructed yet, and that'll point you to the solution.

Resources