Search code examples
multithreadingdelphiindy10delphi-10.3-rio

Creating an object (with a timer) inside a TidHTTPServer.OnCommandGet fails


Inside a TidHTTPServer.OnCommandGet I create a new object. This Object has a timer that should start immediately but doesnt. The TimerEVent never fires! When I create the object somewhere else it works...

Some code

  TVolumeFader=Class(TObject)

...
constructor TVolumeFader.Create(...);
begin
  inherited Create;
  ...
  
    VolTimer:=TTimer.Create(NIL);
    VolTimer.Enabled:=FALSE;
    VolTimer.Interval:=100;
    VolTimer.OnTimer:=DoTimerTick;
end;

procedure TVolumeFader.DoTimerTick(Sender:TObject);
begin
  LogWrite('TimerTick in VolumeFader',Debug);
  If Assigned(VolTimer)then Begin;
    VolTimer.Enabled:=FALSE;
  End;
  try
    LogWrite('Executing VolumeFade in VolumeFader',Debug);
    VolumeFade;
  finally
    If Assigned(VolTimer)then
      VolTimer.Enabled:=TRUE;
  end;
end;


procedure TMain.OnCommandGet;

Begin;
  TVolumeFader.Create(...);
End;

Solution

  • In your object's constructor, you are creating a TTimer ok, but you are setting its Enabled property to False. So make sure you actually activate the timer once the constructor has exited. Or else change False to True in the constructor.

    That being said, your code still won't work as shown. This is because TIdHTTPServer is a multi-threaded component, its OnCommand... events are fired in the context of worker threads that TIdHTTPServer creates for itself when clients connect to the server. But TTimer is a message-based timer, it creates an internal HWND for itself which is tied to the thread that it is created in, and that thread must have a message loop in order for TTimer to process WM_TIMER messages. The worker thread that you are creating your TTimer in does not have a message loop, so the TTimer will not be able to fire its OnTimer event.

    So, you will have to either:

    1. run your own message loop inside of the OnCommand... event handler after creating your object, and then free the object before the event handler exits. There is no guarantee that the calling thread will continue running once the OnCommand... event handler has exited. Just note that the client will be blocked from sending any further HTTP commands to the server while the timer is running:
    procedure TMain.OnCommandGet(AContext: TIdContext;
      ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
    var
      fader: TVolumeFader;
      msg: tagMSG;
    begin
      ...
      fader := TVolumeFader.Create(...);
      try
        while (timer should keep running) do
        begin
          //Application.ProcessMessages;
          if PeekMessage(@msg, 0, 0, 0, PM_REMOVE) then
          begin
            TranslateMessage(@msg);
            DispatchMessage(@msg);
          end else
            Sleep(100);
        end;
      finally
        fader.Free;
      end;
      ...
    end;
    
    1. delegate the creation of your object, and thus its TTimer, to your main UI thread, so that your OnTimer event handler will be fired in the context of that thread rather than in the context of the server's worker thread. Just make sure that your OnTimer code is thread-safe, if it needs to access anything that is shared with the server:
    procedure TMain.OnCommandGet(AContext: TIdContext;
      ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
    begin
      ...
      TThread.Synchronize(nil, // or TThread.Queue()
        procedure
        begin
          TVolumeFader.Create(...); // when do you destroy this object?
        end
      );
      ...
    end;