I've build a datasnap server application for handling data between a windows application and mobile apps.
One method can take a while, and I want to be able to stop it after a certain time(Timeout).
How can I achieve this?
The code below shows one way to provide a server method with timeout behaviour.
The task which may take too long is executed in a secondary thread which is
started in the server method. This method uses a TSimpleEvent object (see the online help) to enable the
secondary thread to signal back to the server method's thread that it has completed. The value (in milliseconds) you specify in the call to Event.WaitFor defines how long to wait before the call times out.
If the call to WaitFor on the SimpleEvent times out, you can take whatever action you
like to notify the server's client. If the call to WaitFor returns wsSignaled, that means that the DBThread must have called SetEvent on the Event object before the period specified when calling WaitFor expired.
Btw, this example was written for D7, so might require minor adaptation for
Seattle. Also it uses a TForm descendant as the "server", but should work equally well in a DataSnap server method, since the principle is the same.
It doesn't address the issue of how exactly to stop whatever task you kick off in the secondary thread, because whether that is possible and how to do it if it is depends on exactly what the task is. Because of that, and the fact that you probably wouldn't want to delay the server method by waiting for the DBThread to complete, it does not attempt to free the DBThread, though in the real world that should of course be done.
type
TServer = class;
TDBThread = class(TThread)
private
FServer: TServer;
FEvent: TSimpleEvent;
FCancelled : Boolean;
function GetCancelled: Boolean;
procedure SetCancelled(const Value: Boolean);
public
procedure Execute; override;
constructor Create(AServer : TServer);
property Server : TServer read FServer;
property Event : TSimpleEvent read FEvent;
property Cancelled : Boolean read GetCancelled write SetCancelled;
end;
TServer = class(TForm)
// ignore the fact that in this case, TServer is a descendant of TForm
Button1: TButton;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
protected
CS : TCriticalSection;
Event : TSimpleEvent;
public
procedure DoServerMethod;
end;
[...]
{ TDBThread }
constructor TDBThread.Create(AServer: TServer);
begin
inherited Create(True); // create suspended
FreeOnTerminate := False;
FServer := AServer;
FEvent := FServer.Event;
end;
procedure TDBThread.Execute;
var
StartTime : Cardinal;
begin
Cancelled := False;
// Following is for illustration ONLY, to simulate a process which takes time.
// Do not call Sleep() in a loop in a real thread
StartTime := GetTickCount;
repeat
Sleep(100);
until GetTickCount - StartTime > 5000;
if not Cancelled then begin
{ TODO : Transfer result back to server thread }
Event.SetEvent;
end;
end;
function TDBThread.GetCancelled: Boolean;
begin
FServer.CS.Enter;
try
Result := FCancelled;
finally
FServer.CS.Leave;
end;
end;
procedure TDBThread.SetCancelled(const Value: Boolean);
begin
FServer.CS.Enter;
try
FCancelled := Value;
finally
FServer.CS.Leave;
end;
end;
procedure TServer.DoServerMethod;
var
DBThread : TDBThread;
WaitResult : TWaitResult;
begin
DBThread := TDBThread.Create(Self);
DBThread.Resume;
WaitResult := Event.WaitFor(1000);
case WaitResult of
wrSignaled : begin
// the DBThread completed
ShowMessage('DBThread completed');
end;
wrTimeOut : begin
// the DBThread time out
DBThread.Cancelled := True;
ShowMessage('DBThread timed out');
// Maybe use PostThreadMessage here to tell the DBThread to abort (if possible)
// whatever task it is doing that has taken too long.
end;
end; {case}
{ TODO : Terminate and dispose of the DBThread }
end;
procedure TServer.FormCreate(Sender: TObject);
begin
CS := TCriticalSection.Create;
Event := TSimpleEvent.Create;
end;
procedure TServer.Button1Click(Sender: TObject);
begin
DoServerMethod;
end;
Related
I write a basic Delphi MS-Windows service.
I install it with the /install directove. This works.
In the Windows Services list it exists.
I START it from there. Windows says it started successfully. It shows as running.
But nothing is executed, except the OnCreate and OnDestroy.
It is in fact NOT running, while Windows claims it IS running.
I tried Delpi 10.2 and the latest 10.4.
What is going wrong here? It is the most basic Service possible.
The Log output looks like this:
Create
AfterInstall
Destroy
Create
Destroy
Create
Destroy
program BartServiceTwo;
uses
Vcl.SvcMgr,
Unit1 in 'Unit1.pas' {BartService: TService};
{$R *.RES}
begin
// Windows 2003 Server requires StartServiceCtrlDispatcher to be
// called before CoRegisterClassObject, which can be called indirectly
// by Application.Initialize. TServiceApplication.DelayInitialize allows
// Application.Initialize to be called from TService.Main (after
// StartServiceCtrlDispatcher has been called).
//
// Delayed initialization of the Application object may affect
// events which then occur prior to initialization, such as
// TService.OnCreate. It is only recommended if the ServiceApplication
// registers a class object with OLE and is intended for use with
// Windows 2003 Server.
//
// Application.DelayInitialize := True;
//
if not Application.DelayInitialize or Application.Installing then
Application.Initialize;
Application.CreateForm(TBartService, BartService);
Application.Run;
end.
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Classes, Vcl.SvcMgr;
type
TBartService = class(TService)
procedure ServiceExecute(Sender: TService);
procedure ServiceCreate(Sender: TObject);
procedure ServiceDestroy(Sender: TObject);
procedure ServiceStart(Sender: TService; var Started: Boolean);
procedure ServiceStop(Sender: TService; var Stopped: Boolean);
procedure ServiceAfterInstall(Sender: TService);
private
{ Private declarations }
public
function GetServiceController: TServiceController; override;
procedure Log(Line:string);
{ Public declarations }
end;
var
BartService: TBartService;
LogFile: text;
Logfilename: string;
implementation
{$R *.dfm}
procedure TBartService.Log(Line:string);
begin
if Logfilename = '' then
begin
Logfilename := 'Log.txt';
Assignfile(LogFile,Logfilename);
end;
try
if FileExists(Logfilename)
then append(LogFile)
else rewrite(LogFile);
writeln(LogFile,line);
Closefile(LogFile);
except
on E:Exception do;
end;
end;
procedure ServiceController(CtrlCode: DWord); stdcall;
begin
BartService.Controller(CtrlCode);
end;
function TBartService.GetServiceController: TServiceController;
begin
Result := ServiceController;
end;
procedure TBartService.ServiceAfterInstall(Sender: TService);
begin
Log('AfterInstall');
end;
procedure TBartService.ServiceCreate(Sender: TObject);
begin
Log('Create');
messagebeep(0);
end;
procedure TBartService.ServiceDestroy(Sender: TObject);
begin
Log('Destroy');
end;
procedure TBartService.ServiceExecute(Sender: TService);
begin
Log('ServiceExecute Start. Terminated='+Terminated.ToString(true));
while not Terminated do
begin
try
ServiceThread.ProcessRequests(false);
Log('ServiceExecute');
// messagebeep(0);
sleep(1000);
except
on E:Exception do
begin
Log('ERROR: ServiceExecute: Final: '+E.Message);
end;
end;
end;
Log('ServiceExecute Out of loop.');
end;
procedure TBartService.ServiceStart(Sender: TService; var Started: Boolean);
begin
Log('ServiceStart');
end;
procedure TBartService.ServiceStop(Sender: TService; var Stopped: Boolean);
begin
Log('ServiceStop');
end;
end.
I assume that during your debugging you have copy and pasted code into the unit from another project but you have not 'hooked up' the events properly. Bring up the project in Delphi and open the service module. Click on the Events tab in the Object Inspector and my guess is that they are all blank. (View the source of the .dfm and there is likely no OnExecute, OnStop, OnStop, etc events defined)
For example - double click the OnExecute event and I assume the IDE will automatically create a new OnExecute event rather than navigating to your OnExecute event in the unit.
Simply rehook up your events and it will most likely work as expected.
Solved. After using the 'LogMessage() system, I found that the service is in fact running.
But what happened, is that the destination folder of my simple Log file was transfered from the local executable directory to C:\Windows\System32\ and there was all the rest of the Log data... I never expected that :(
Thanks for all help, Bart
I have a room with 60 computers/devices (40 computers and 20 oscilloscopes Windows CE based) and I would like to know which and every one is alive using ping. First I wrote a standard ping (see here Delphi Indy Ping Error 10040), which is working fine now but takes ages when most computers are offline.
So what I am trying to do is to write a MultiThread Ping but I am quite struggling with it. I have seen only very few examples over the internet and no one was matching my needs, that's why I try to write it myself.
I use XE2 and Indy 10 and the form is only constitued of a memo and a button.
unit Main;
interface
uses
Winapi.Windows, System.SysUtils, System.Classes, Vcl.Forms,
IdIcmpClient, IdGlobal, Vcl.StdCtrls, Vcl.Controls;
type
TMainForm = class(TForm)
Memo1: TMemo;
ButtonStartPing: TButton;
procedure ButtonStartPingClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
type
TMyPingThread = class(TThread)
private
fIndex : integer;
fIdIcmpClient: TIdIcmpClient;
procedure doOnPingReply;
protected
procedure Execute; override;
public
constructor Create(index: integer);
end;
var
MainForm: TMainForm;
ThreadCOunt : integer;
implementation
{$R *.dfm}
constructor TMyPingThread.Create(index: integer);
begin
inherited Create(false);
fIndex := index;
fIdIcmpClient := TIdIcmpClient.Create(nil);
fIdIcmpClient.ReceiveTimeout := 200;
fIdIcmpClient.PacketSize := 24;
fIdIcmpClient.Protocol := 1;
fIdIcmpClient.IPVersion := Id_IPv4;
//first computer is at adresse 211
fIdIcmpClient.Host := '128.178.26.'+inttostr(211+index-1);
self.FreeOnTerminate := true;
end;
procedure TMyPingThread.doOnPingReply;
begin
MainForm.Memo1.lines.add(inttostr(findex)+' '+fIdIcmpClient.ReplyStatus.Msg);
dec(ThreadCount);
if ThreadCount = 0 then
MainForm.Memo1.lines.add('--- End ---');
end;
procedure TMyPingThread.Execute;
begin
inherited;
try
fIdIcmpClient.Ping('',findex);
except
end;
while not Terminated do
begin
if fIdIcmpClient.ReplyStatus.SequenceId = findex then Terminate;
end;
Synchronize(doOnPingReply);
fIdIcmpClient.Free;
end;
procedure TMainForm.ButtonStartPingClick(Sender: TObject);
var
i: integer;
myPing : TMyPingThread;
begin
Memo1.Lines.Clear;
ThreadCount := 0;
for i := 1 to 40 do
begin
inc(ThreadCount);
myPing := TMyPingThread.Create(i);
//sleep(10);
end;
end;
end.
My problem is that it "seems" to work when I uncomment the "sleep(10)", and "seems" not to be working without it. This for sure means I am missing a point in the threading I have written.
In other words. When Sleep(10) is in the code. Every time I clicked the button to get to check the connections the result was correct.
Without the sleep(10), it is working "most" of the time but some times the result is wrong giving me a ping echo on offline computers and no ping echo on online computer, as is the ping reply was not assigned to the correct thread.
Any comment or help is welcome.
----- EDIT / IMPORTANT -----
As a general follow up of this question, #Darian Miller started a Google Code project here https://code.google.com/p/delphi-stackoverflow/ which is a working basis. I mark his answer as the "accepted answer" but users should refer to this open source project (all the credit belongs to him) as it will surely be extended and updated in the future.
The root problem is that pings are connectionless traffic. If you have multiple TIdIcmpClient objects pinging the network at the same time, one TIdIcmpClient instance can receive a reply that actually belongs to another TIdIcmpClient instance. You are trying to account for that in your thread loop, by checking SequenceId values, but you are not taking into account that TIdIcmpClient already does that same check internally. It reads network replies in a loop until it receives the reply it is expecting, or until the ReceiveTimeout occurs. If it receives a reply it is not expecting, it simply discards that reply. So, if one TIdIcmpClient instance discards a reply that another TIdIcmpClient instance was expecting, that reply will not get processed by your code, and that other TIdIcmpClient will likely receive another TIdIcmpClient's reply instead, and so on. By adding the Sleep(), you are decreasing (but not eliminating) the chances that pings will overlap each other.
For what you are attempting to do, you won't be able to use TIdIcmpClient as-is to have multiple pings running in parallel, sorry. It is simply not designed for that. There is no way for it to differentiate reply data the way you need it. You will have to serialize your threads so only one thread can call TIdIcmpClient.Ping() at a time.
If serializing the pings is not an option for you, you can try copying portions of TIdIcmpClient's source code into your own code. Have 41 threads running - 40 device threads and 1 response thread. Create a single socket that all of the threads share. Have each device thread prepare and send its individual ping request to the network using that socket. Then have the response thread continuously reading replies from that same socket and routing them back to the appropriate device thread for processing. This is a bit more work, but it will give you the multiple-ping parallelism you are looking for.
If you don't want to go to all that trouble, an alternative is to just use a third-party app that already supports pinging multiple machines at the same time, like FREEPing.
Remy explained the problems... I've wanted to do this in Indy for a while so I posted a possible solution that I just put together to a new Google Code project instead of having a long comment here. It's a first-stab sort of thing, let me know if you have some changes to integrate:
https://code.google.com/p/delphi-vault/
This code has two ways to Ping...multi-threaded clients as in your example, or with a simple callback procedure. Written for Indy10 and later versions of Delphi.
Your code would end up using a TThreadedPing descendant defining a SynchronizedResponse method:
TMyPingThread = class(TThreadedPing)
protected
procedure SynchronizedResponse(const ReplyStatus:TReplyStatus); override;
end;
And to fire off some client threads, the code becomes something like:
procedure TfrmThreadedPingSample.butStartPingClick(Sender: TObject);
begin
TMyPingThread.Create('www.google.com');
TMyPingThread.Create('127.0.0.1');
TMyPingThread.Create('www.shouldnotresolvetoanythingatall.com');
TMyPingThread.Create('127.0.0.1');
TMyPingThread.Create('www.microsoft.com');
TMyPingThread.Create('127.0.0.1');
end;
The threaded response is called in a synchronized method:
procedure TMyPingThread.SynchronizedResponse(const ReplyStatus:TReplyStatus);
begin
frmThreadedPingSample.Memo1.Lines.Add(TPingClient.FormatStandardResponse(ReplyStatus));
end;
I did not try your code, so that is all hypothetical, but i think you messed the threads and got classic race condition. I restate my advice to use AsyncCalls or OmniThreadLibrary - they are much simpler and would save you few attempts at "shooting your own foot".
Threads are made to minimize main-thread load. Thread constructor should do minimal work of remembering parameters. Personally i'd moved idICMP creation into .Execute method. If for any reason it would want to create its internal synchronization objects, like window and message queue or signal or whatever, i'd like it to happen already in a new spawned thread.
There is no sense for "inherited;" in .Execute. Better remove it.
Silencing all exceptions is bad style. You probably have errors - but have no way to know about them. You should propagate them to main thread and display them. OTL and AC help you in that, while for tThread you have to do it manually. How to Handle Exceptions thrown in AsyncCalls function without calling .Sync?
Exception logic is flawed. There is no point to have a loop if exception thrown - if no succesful Ping was set - then why waiting for response ? You loop should go within same try-except frame as issuing ping.
Your doOnPingReply executes AFTER fIdIcmpClient.Free yet accesses fIdIcmpClient's internals. Tried changing .Free for FreeAndNil ?
That is a classic mistake of using dead pointer after freeing it.
The correct approach would be to:
5.1. either free the object in doOnPingReply
5.2. or copy all relevant data from doOnPingReply to TThread's private member vars before calling both Synchronize and idICMP.Free (and only use those vars in doOnPingReply )
5.3. only do fIdIcmpClient.Free inside TMyThread.BeforeDestruction or TMyThread.Destroy. Afterall, if you chosen to create the object in constructor - then you should free it in the matching language construct - destructor.
Since you do not keep references to the thread objects - that While not Terminated loop seems redundant. Just make usual forever-loop and call break.
The aforementioned loop is CPU-hungry, it is like spin-loop. Please call Sleep(0); or Yield(); inside loop to give other threads better chance to do their work. Don't work agaisnt OS scheduler here - you are not in a speed-critical path, no reason to make spinlock here.
Overall, i consider:
4 and 5 as critical bugs for you
1 and 3 as a potential gotcha maybe influencing or maybe not. You'd better 'play safe' rather than doing risky things and investigating if they would work or not.
2 and 7 - bad style, 2 regarding language and 7 regarding platform
6 either you have plans to extend your app, or you broke YAGNI principle, dunno.
Sticking with complex TThread instead of OTL or AsyncCalls - strategic errors. Don't you put rooks on your runway, use simple tools.
Funny, this is example of the bug that FreeAndNil could expose and make obvious, while FreeAndNil-haters are claiming it "conceals" bugs.
// This is my communication unit witch works well, no need to know its work but your
// ask is in the TPingThread class.
UNIT UComm;
INTERFACE
USES
Windows, Messages, SysUtils, Classes, Graphics, Controls, ExtCtrls, Forms, Dialogs,
StdCtrls,IdIcmpClient, ComCtrls, DB, abcwav, SyncObjs, IdStack, IdException,
IdTCPServer, IdBaseComponent, IdComponent, IdTCPConnection, IdTCPClient, IdContext,
UDM, UCommon;
TYPE
TNetworkState = (nsNone, nsLAN, nsNoLAN, nsNet, nsNoNet);
TDialerStatus = (dsNone, dsConnected, dsDisconnected, dsNotSync);
{ TBaseThread }
TBaseThread = Class(TThread)
Private
FEvent : THandle;
FEventOwned : Boolean;
Procedure ThreadTerminate(Sender: TObject); Virtual;
Public
Constructor Create(AEventName: String);
Property EventOwned: Boolean Read FEventOwned;
End;
.
.
.
{ TPingThread }
TPingThread = Class(TBaseThread)
Private
FReply : Boolean;
FTimeOut : Integer;
FcmpClient : TIdIcmpClient;
Procedure ReplyEvent(Sender: TComponent; Const AReplyStatus: TReplyStatus);
Protected
Procedure Execute; Override;
Procedure ThreadTerminate(Sender: TObject); Override;
Public
Constructor Create(AHostIP, AEventName: String; ATimeOut: Integer);
Property Reply: Boolean Read FReply;
End;
.
.
.
{ =============================================================================== }
IMPLEMENTATION
{$R *.dfm}
USES
TypInfo, WinSock, IdGlobal, UCounter, UGlobalInstance, URemoteDesktop;
{IdGlobal: For RawToBytes function 10/07/2013 04:18 }
{ TBaseThread }
//---------------------------------------------------------
Constructor TBaseThread.Create(AEventName: String);
Begin
SetLastError(NO_ERROR);
FEvent := CreateEvent(Nil, False, False, PChar(AEventName));
If GetLastError = ERROR_ALREADY_EXISTS
Then Begin
CloseHandle(FEvent);
FEventOwned := False;
End
Else If FEvent <> 0 Then
Begin
FEventOwned := True;
Inherited Create(True);
FreeOnTerminate := True;
OnTerminate := ThreadTerminate;
End;
End;
//---------------------------------------------------------
Procedure TBaseThread.ThreadTerminate(Sender: TObject);
Begin
CloseHandle(FEvent);
End;
{ TLANThread }
.
.
.
{ TPingThread }
//---------------------------------------------------------
Constructor TPingThread.Create(AHostIP: String; AEventName: String; ATimeOut: Integer);
Begin
Inherited Create(AEventName);
If Not EventOwned Then Exit;
FTimeOut := ATimeOut;
FcmpClient := TIdIcmpClient.Create(Nil);
With FcmpClient Do
Begin
Host := AHostIP;
ReceiveTimeOut := ATimeOut;
OnReply := ReplyEvent;
End;
End;
//---------------------------------------------------------
Procedure TPingThread.Execute;
Begin
Try
FcmpClient.Ping;
FReply := FReply And (WaitForSingleObject(FEvent, FTimeOut) = WAIT_OBJECT_0);
Except
FReply := False;
End;
End;
//---------------------------------------------------------
Procedure TPingThread.ReplyEvent(Sender: TComponent; Const AReplyStatus: TReplyStatus);
Begin
With AReplyStatus Do
FReply := (ReplyStatusType = rsEcho) And (BytesReceived <> 0);
SetEvent(FEvent);
End;
//---------------------------------------------------------
Procedure TPingThread.ThreadTerminate(Sender: TObject);
Begin
FreeAndNil(FcmpClient);
Inherited;
End;
{ TNetThread }
.
.
.
In order to learn multithreading, I've created a thread inside a COM Thread (TRemoteDataModule).
This is my Component Factory:
TComponentFactory.Create(ComServer, TServerConn2, Class_ServerConn2, ciMultiInstance, tmApartment);
Inside the Thread, I didn't needed to Call CoInitialize to use TADOQuery.Create, .Open... .Exec
I read that I need to initialize the COM library on a thread before you call any of the library functions except CoGetMalloc, to get a pointer to the standard allocator, and the memory allocation functions.
But in this case, the absence of CoInitialize didn't brought me any trouble.
Is this related with Thread Model?
Where can I Find the explanation for this subject?
UPDATE:
When I say INSIDE, it means inside the COM method context:
interface
type
TWorker = class(TThread);
TServerConn2 = class(TRemoteDataModule, IServerConn2)
public
procedure Method(); safecall;
end;
implementation
procedure TServerConn2.Method();
var W: TWorker;
begin
W := TWorkerTread.Create(Self);
end;
UPDATE 2:
The TADOConnection used to connect to database are currently being created in the COM Thread context (TThread.Create constructor). Although, TADOConnection.Open and TADOQuery.Create/.Open are both being performed inside TThread.Execute .
UPDATE 3 - Simulacrum
Interface:
type
TServerConn2 = class;
TWorker = class(TThread)
private
FDB: TADOConnection;
FOwner: TServerConn2;
protected
procedure Execute; override;
public
constructor Create(Owner: TServerConn2);
destructor Destroy; override;
end;
TServerConn2 = class(TRemoteDataModule, IServerConn2)
ADOConnection1: TADOConnection;
procedure RemoteDataModuleCreate(Sender: TObject);
private
{ Private declarations }
protected
class procedure UpdateRegistry(Register: Boolean; const ClassID, ProgID: string); override;
procedure CheckException; safecall;
public
User, Pswd, Str: String;
Ok: Boolean;
end;
Implementation:
class procedure TServerConn2.UpdateRegistry(Register: Boolean; const ClassID, ProgID: string);
begin
if Register then
begin
inherited UpdateRegistry(Register, ClassID, ProgID);
EnableSocketTransport(ClassID);
EnableWebTransport(ClassID);
end else
begin
DisableSocketTransport(ClassID);
DisableWebTransport(ClassID);
inherited UpdateRegistry(Register, ClassID, ProgID);
end;
end;
{ TWorker }
constructor TWorker.Create(Owner: TServerConn2);
begin
inherited Create(False);
FreeOnTerminate := True;
FDB := TADOConnection.Create(nil);
FOwner := Owner;
end;
destructor TWorker.Destroy;
begin
FDB.Free;
FOwner.Ok := True;
inherited;
end;
procedure TWorker.Execute;
var Qry: TADOQuery;
begin
FDB.LoginPrompt := False;
FDB.ConnectionString := FOwner.Str;
FDB.Open(FOwner.User, FOwner.Pswd);
Qry := TADOQuery.Create(nil);
try
Qry.Connection := FDB;
Qry.LockType := ltReadOnly;
Qry.SQL.Text := 'SELECT TOP 1 * FROM MyTable';
Qry.Open;
finally
Qry.Free;
end;
end;
procedure TServerConn2.CheckException;
var W: TWorker;
begin
W := TWorker.Create(Self);
while not Ok do Sleep(100);
end;
procedure TServerConn2.RemoteDataModuleCreate(Sender: TObject);
begin
User := 'user';
Pswd := 'pass';
Str := ADOConnection1.ConnectionString;
end;
initialization
TComponentFactory.Create(ComServer, TServerConn2,
Class_ServerConn2, ciMultiInstance, tmApartment);
end.
UPDATE 4
The error should happen here:
function CreateADOObject(const ClassID: TGUID): IUnknown;
var
Status: HResult;
FPUControlWord: Word;
begin
asm
FNSTCW FPUControlWord
end;
Status := CoCreateInstance(ClassID, nil, CLSCTX_INPROC_SERVER or
CLSCTX_LOCAL_SERVER, IUnknown, Result);
asm
FNCLEX
FLDCW FPUControlWord
end;
if (Status = REGDB_E_CLASSNOTREG) then
raise Exception.CreateRes(#SADOCreateError) else
OleCheck(Status);
end;
By somehow (because of TComponentFactory maybe?) CoCreateInstance identifies that TWorker is in the same context than TServerConn2 and don't raise errors?
Either or both of the following might apply:
On a thread not initialized with COM all existing interface pointers keep working until you make a COM API call or otherwise require COM marshalling which then fails detecting an uninitialized thread. That is, your "didn't brought me any trouble" might actually be too early to say.
If any thread in the process calls CoInitialize[Ex] with the COINIT_MULTITHREADED flag, then that not only initializes the current thread as a member of the multi-threaded apartment, but it also says, "Any thread which has never called CoInitialize[Ex] is also part of the multi-threaded apartment." - so called impicit MTA thing
The TADOConnection used to connect to database are currently being created in the COM Thread context (TThread.Create constructor). Although, TADOConnection.Open and TADOQuery.Create/.Open are both being performed inside TThread.Execute .
That will not work, for 2 reasons:
TWorker.Create() and TWorker.Execute() will run in different thread contexts. Create() will run in the context of the thread that is calling TServerConn2.CheckException() (which will have already called CoInitialize/Ex() on itself beforehand), but Execute() will run in the context of the TThread thread instead. ADO is apartment threaded, which means its COM interfaces cannot be used across thread/apartment boundaries unless you marshal them, either via the IGlobalInterfaceTable interface or the CoMarshalInterThreadInterfaceInStream() and CoGetInterfaceAndReleaseStream() functions.
even if you did marshal the ADO interfaces, TWorker.Execute() must call CoInitialize/Ex() on itself. EVERY individual thread must initialize COM to establish its threading model before then accessing any COM interfaces. The threading model dictates how COM accesses interfaces (direct or through proxies), whether message queues are used, etc.
So the simple solution to your problem is to NOT create and use the ADO components across thread boundaries at all. Move your TADOConnection into Execute() instead:
constructor TWorker.Create(Owner: TServerConn2);
begin
inherited Create(False);
FreeOnTerminate := True;
FOwner := Owner;
end;
destructor TWorker.Destroy;
begin
FOwner.Ok := True;
inherited;
end;
procedure TWorker.Execute;
var
DB: TADOConnection;
Qry: TADOQuery;
begin
CoInitialize;
try
DB := TADOConnection.Create(nil);
try
DB.LoginPrompt := False;
DB.ConnectionString := FOwner.Str;
DB.Open(FOwner.User, FOwner.Pswd);
Qry := TADOQuery.Create(nil);
try
Qry.Connection := DB;
Qry.LockType := ltReadOnly;
Qry.SQL.Text := 'SELECT TOP 1 * FROM MyTable';
Qry.Open;
finally
Qry.Free;
end;
finally
DB.Free;
end;
finally
CoUninitialize;
end;
end;
When you create an apartment thread using TComponentFactory it calls CoInitialize and CoUnInitialize for you - it's right in the VCL source (System.Win.VCLCom.pas):
procedure TApartmentThread.Execute;
var
msg: TMsg;
Unk: IUnknown;
begin
try
CoInitialize(nil); // *** HERE
try
FCreateResult := FFactory.CreateInstanceLic(FUnkOuter, nil, FIID, '', Unk);
FUnkOuter := nil;
FFactory := nil;
if FCreateResult = S_OK then
CoMarshalInterThreadInterfaceInStream(FIID, Unk, IStream(FStream));
ReleaseSemaphore(FSemaphore, 1, nil);
if FCreateResult = S_OK then
while GetMessage(msg, 0, 0, 0) do
begin
DispatchMessage(msg);
Unk._AddRef;
if Unk._Release = 1 then break;
end;
finally
Unk := nil;
CoUninitialize; // ** AND HERE
end;
except
{ No exceptions should go unhandled }
end;
end;
First go at starting my own service in Delphi 7. Followed the docs and made the service spawn a custom thread that beeps and logs. Only it doesn't. Last attempt was to put the same beep and log code in OnExecute event procedure, but when I start the service I get a Windows dialog saying that it was started and then stopped again.
There should be something obvious that I've overlooked in this code.
Could you have a look? I'll also accept links to simple, working, downloadable service example projects... just so I get something that is called every 10 seconds or so and I'll take it from there.
A bare bones service application follows.
Please note that if you want to install the service on Windows Vista and higher using ServiceApp.exe /install, you will have to ensure that you are running the app with administrator rights.
Also note that despite the fmShareDenyWrite the contents of the log file may not be viewable while the service is running. At least I couldn't open the file using Notepad++ until after I stopped the service. This may have to do with the fact that I had the service running under the system account (as opposed to my own user account).
One other remark:
If you want to allow your service to be paused and continued, don't use suspend and resume. They are not thread safe and have been deprecated in D2010+. Using T(Simple)Event or something similar to control the main worker thread's execution.
If you do not want to allow your service to be paused and continued, you can simply set AllowPause to False.
unit ServiceApp_fm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, SvcMgr, Dialogs;
type
TService1 = class(TService)
procedure ServiceStart(Sender: TService; var Started: Boolean);
procedure ServiceStop(Sender: TService; var Stopped: Boolean);
private
FWorker: TThread;
public
function GetServiceController: TServiceController; override;
end;
var
Service1: TService1;
implementation
{$R *.DFM}
type
TMainWorkThread = class(TThread)
private
{$IFDEF UNICODE}
FLog: TStreamWriter;
{$ELSE}
FLog: TFileStream;
{$ENDIF}
FRepetition: Cardinal;
public
constructor Create;
destructor Destroy; override;
procedure Execute; override;
end;
procedure ServiceController(CtrlCode: DWord); stdcall;
begin
Service1.Controller(CtrlCode);
end;
function TService1.GetServiceController: TServiceController;
begin
Result := ServiceController;
end;
procedure TService1.ServiceStart(Sender: TService; var Started: Boolean);
begin
FWorker := TMainWorkThread.Create;
Started := True;
end;
procedure TService1.ServiceStop(Sender: TService; var Stopped: Boolean);
begin
// Thread should be freed as well as terminated so we don't have a memory
// leak. Use FreeAndNil so we can also recognize when the thread isn't
// available. (When the service has been stopped but the process hasn't ended
// yet or may not even end when the service is restarted instead of "just" stopped.
if FWorker <> nil then
begin
FWorker.Terminate;
while WaitForSingleObject(FWorker.Handle, WaitHint-100) = WAIT_TIMEOUT do
ReportStatus;
FreeAndNil(FWorker);
end;
Stopped := True;
end;
{ TMainWorkThread }
constructor TMainWorkThread.Create;
var
FileName: String;
begin
inherited Create({CreateSuspended=}False);
FileName := ExtractFilePath(ParamStr(0)) + '\WorkerLog.txt';
{$IFDEF UNICODE}
FLog := TStreamWriter.Create(FileName, False, TEncoding.Unicode);
{$ELSE}
FLog := TFileStream.Create(FileName, fmCreate);
{$ENDIF}
end;
destructor TMainWorkThread.Destroy;
begin
FLog.Free;
inherited;
end;
procedure TMainWorkThread.Execute;
var
Text: string;
begin
inherited;
while not Terminated do begin
Inc(FRepetition);
Text := Format('Logging repetition %d'#13#10, [FRepetition]);
{$IFDEF UNICODE}
FLog.Write(Text);
{$ELSE}
FLog.Write(Text[1], Length(Text));
{$ENDIF}
Sleep(1000);
end;
end;
end.
Please have a look at http://www.delphi3000.com/articles/article_3379.asp for details on creating a service. I made that post years ago, but should still work.
Remove below method event
procedure TAviaABSwedenAMailer.ServiceExecute(Sender: TService);
begin
while not Terminated do
begin
Beep;
Sleep(500);
LG('Amailer is running');
ServiceThread.ProcessRequests(False);
end;
end;
The beep will not work, see this post.
Your procedure LG is not verry robust it may fail if the log file doesn't exist. Also the service user must have the right to access the file. In a first step you can run the service with your user account for testing.
I need to determine which folders contain files that have been modified "recently" (within a certain interval). I notice that folder datestamps seem to get updated whenever a contained file is modified, but this behaviour doesn't propagate up the tree, i.e. the datestamp of the folder containing the folder that contains the modified file doesn't get updated.
I can work with this behaviour, but I suspect it depends on platform/file system/network or local drive, etc. I would still like to take advantage of it where I could, so I need a boolean function to return true if the platform/disk running my app supports this behaviour.
I'm quite happy to recurse through the tree. What I want to avoid is having to do a FindFirst/FindNext for every file in every folder to see if any have been modified in (say) the last day - if I can avoid doing that for folders that don't have their datestamps modified within the last day it will save a great deal of time.
Check the FindFirstChangeNotification and FindNextChangeNotification functions
another option is use the TJvChangeNotify JEDI component.
addionally you can check this link
Obtaining Directory Change Notifications
The solutions that have been posted so far are about obtaining notifications as they happen, and they'll work well for that purpose. If you want to look into the past and see when something was last changed, as opposed to monitoring it in real time, then it gets tricker. I think there's no way to do that except by recursively searching through the folder tree and checking datestamps.
EDIT: In response to the OP's comment, yeah, it doesn't look like there's any way to configure FindFirst/FindNext to only hit directories and not files. But you can skip checking the dates on the files with this filter: (SearchRec.Attr and SysUtils.faDirectory <> 0). That should speed things up a little. Don't check the dates on the files at all. You'll probably still have to scan through everything, though, since the Windows API doesn't provide any way (that I know of) to only query for folders and not files.
I wrote a code for this purpose for one of my projects. This uses FindFirstChangeNotification and FindNextChangeNotification API functions.
Here is the code (I removed some project specific portions):
/// <author> Ali Keshavarz </author>
/// <date> 2010/07/23 </date>
unit uFolderWatcherThread;
interface
uses
SysUtils, Windows, Classes, Generics.Collections;
type
TOnThreadFolderChange = procedure(Sender: TObject; PrevModificationTime, CurrModificationTime: TDateTime) of object;
TOnThreadError = procedure(Sender: TObject; const Msg: string; IsFatal: Boolean) of object;
TFolderWatcherThread = class(TThread)
private
class var TerminationEvent : THandle;
private
FPath : string;
FPrevModificationTime : TDateTime;
FLatestModification : TDateTime;
FOnFolderChange : TOnThreadFolderChange;
FOnError : TOnThreadError;
procedure DoOnFolderChange;
procedure DoOnError(const ErrorMsg: string; IsFatal: Boolean);
procedure HandleException(E: Exception);
protected
procedure Execute; override;
public
constructor Create(const FolderPath: string;
OnFolderChangeHandler: TOnThreadFolderChange;
OnErrorHandler: TOnThreadError);
destructor Destroy; override;
class procedure PulseTerminationEvent;
property Path: string read FPath;
property OnFolderChange: TOnThreadFolderChange read FOnFolderChange write FOnFolderChange;
property OnError: TOnThreadError read FOnError write FOnError;
end;
/// <summary>
/// Provides a list container for TFolderWatcherThread instances.
/// TFolderWatcherThreadList can own the objects, and terminate removed items
/// automatically. It also uses TFolderWatcherThread.TerminationEvent to unblock
/// waiting items if the thread is terminated but blocked by waiting on the
/// folder changes.
/// </summary>
TFolderWatcherThreadList = class(TObjectList<TFolderWatcherThread>)
protected
procedure Notify(const Value: TFolderWatcherThread; Action: TCollectionNotification); override;
end;
implementation
{ TFolderWatcherThread }
constructor TFolderWatcherThread.Create(const FolderPath: string;
OnFolderChangeHandler: TOnThreadFolderChange; OnErrorHandler: TOnThreadError);
begin
inherited Create(True);
FPath := FolderPath;
FOnFolderChange := OnFolderChangeHandler;
Start;
end;
destructor TFolderWatcherThread.Destroy;
begin
inherited;
end;
procedure TFolderWatcherThread.DoOnFolderChange;
begin
Queue(procedure
begin
if Assigned(FOnFolderChange) then
FOnFolderChange(Self, FPrevModificationTime, FLatestModification);
end);
end;
procedure TFolderWatcherThread.DoOnError(const ErrorMsg: string; IsFatal: Boolean);
begin
Synchronize(procedure
begin
if Assigned(Self.FOnError) then
FOnError(Self,ErrorMsg,IsFatal);
end);
end;
procedure TFolderWatcherThread.Execute;
var
NotifierFielter : Cardinal;
WaitResult : Cardinal;
WaitHandles : array[0..1] of THandle;
begin
try
NotifierFielter := FILE_NOTIFY_CHANGE_DIR_NAME +
FILE_NOTIFY_CHANGE_LAST_WRITE +
FILE_NOTIFY_CHANGE_FILE_NAME +
FILE_NOTIFY_CHANGE_ATTRIBUTES +
FILE_NOTIFY_CHANGE_SIZE;
WaitHandles[0] := FindFirstChangeNotification(PChar(FPath),True,NotifierFielter);
if WaitHandles[0] = INVALID_HANDLE_VALUE then
RaiseLastOSError;
try
WaitHandles[1] := TerminationEvent;
while not Terminated do
begin
//If owner list has created an event, then wait for both handles;
//otherwise, just wait for change notification handle.
if WaitHandles[1] > 0 then
//Wait for change notification in the folder, and event signaled by
//TWatcherThreads (owner list).
WaitResult := WaitForMultipleObjects(2,#WaitHandles,False,INFINITE)
else
//Wait just for change notification in the folder
WaitResult := WaitForSingleObject(WaitHandles[0],INFINITE);
case WaitResult of
//If a change in the monitored folder occured
WAIT_OBJECT_0 :
begin
// notifiy caller.
FLatestModification := Now;
DoOnFolderChange;
FPrevModificationTime := FLatestModification;
end;
//If event handle is signaled, let the loop to iterate, and check
//Terminated status.
WAIT_OBJECT_0 + 1: Continue;
end;
//Continue folder change notification job
if not FindNextChangeNotification(WaitHandles[0]) then
RaiseLastOSError;
end;
finally
FindCloseChangeNotification(WaitHandles[0]);
end;
except
on E: Exception do
HandleException(E);
end;
end;
procedure TFolderWatcherThread.HandleException(E: Exception);
begin
if E is EExternal then
begin
DoOnError(E.Message,True);
Terminate;
end
else
DoOnError(E.Message,False);
end;
class procedure TFolderWatcherThread.PulseTerminationEvent;
begin
/// All instances of TFolderChangeTracker which are waiting will be unblocked,
/// and blocked again immediately to check their Terminated property.
/// If an instance is terminated, then it will end its execution, and the rest
/// continue their work.
PulseEvent(TerminationEvent);
end;
{ TFolderWatcherThreadList }
procedure TFolderWatcherThreadList.Notify(const Value: TFolderWatcherThread;
Action: TCollectionNotification);
begin
if OwnsObjects and (Action = cnRemoved) then
begin
/// If the thread is running, terminate it, before freeing it.
Value.Terminate;
/// Pulse global termination event to all TFolderWatcherThread instances.
TFolderWatcherThread.PulseTerminationEvent;
Value.WaitFor;
end;
inherited;
end;
end.
This provides two classes; a thread class which monitors a folder for changes, and if a change is detected, it will return the current change time and the previous change time through OnFolderChange event. And a list class for storing a list of monitoring threads. This list terminates each own thread automatically when the thread is removed from list.
I hope it helps you.
you should have a look at http://help.delphi-jedi.org/item.php?Id=172977 which is a ready solution.
If you do not want to download & install whole JVCL (which is however a great piece of code ;) ) you might want to see the file source online - http://jvcl.svn.sourceforge.net/viewvc/jvcl/trunk/jvcl/run/JvChangeNotify.pas?revision=12481&view=markup