服务应用程序中的PostMessage

时间:2009-02-24 15:07:39

标签: delphi service postmessage

我无法解决问题。我在Delphi中创建了两个服务应用程序,并尝试在其中发布消息。当然,这种应用程序中没有窗口,PostMessage需要窗口句柄参数来发送消息。

因此,我使用AllocateHWnd(MyMethod:TWndMethod)函数创建了一个窗口句柄,并作为“MyMethod”参数传递了一个我希望在收到消息时调用的过程。如果它是一个窗口化的应用程序,使用AllocateHWnd方法返回的句柄调用的PostMessage()肯定会发送一条消息,然后由“MyMethod”过程接收。

但是,我的服务应用程序的情况有所不同。我不明白为什么,但在其中一个发布消息这种方式工作正常,而在第二个它没有(消息根本没有收到)。只有当服务停止时,我才会注意到'MyMethod'收到了两条消息:WM_DESTROY和WM_NCDESTROY。我使用PostMessage发送的消息永远不会被此过程接收。另一方面,第一个服务始终接收我发送的所有消息。

您能告诉我一个线索,以帮助我找到第二次服务没有收到我的消息的原因吗?我不知道他们会以什么方式区别开来。我检查了服务的设置,它们似乎完全相同。为什么然后其中一个工作正常而第二个没有(就发送消息而言)?

感谢您的任何建议。 马里乌什。

8 个答案:

答案 0 :(得分:3)

如果没有更多信息,将很难帮助您调试此问题,尤其是为什么它在一个服务中工作而在另一个服务中不工作。但是:

您可能希望完全删除Windows,而不是尝试修复代码中的问题,而是使用PostThreadMessage()而不是PostMessage()。要使消息发布正常工作,您需要一个消息循环,但不一定是接收窗口。

修改:我试图一次性回复您的所有答案。

首先 - 如果你想让生活更轻松,你应该OmniThreadLibrary查看gabr。我不知道它是否在Windows服务应用程序中有效,我甚至不知道是否已经尝试过。你可以在论坛上提问。它有许多很棒的功能,值得研究,如果只是为了学习效果。

但是当然你也可以自己编程,你必须在Delphi 2007之前使用Delphi版本。我将简单地从我们的内部库中添加一些片段,这些片段已经发展了多年并在几十个程序中运行。我并不认为它是无错误的。您可以将其与您的代码进行比较,如果有任何问题,请随时提出,我会尽力澄清。

这是工作线程基类的简化 Execute()方法:

procedure TCustomTestThread.Execute;
var
  Msg: TMsg;
begin
  try
    while not Terminated do begin
      if (integer(GetMessage(Msg, HWND(0), 0, 0)) = -1) or Terminated then
        break;
      TranslateMessage(Msg);
      DispatchMessage(Msg);

      if Msg.Message = WM_USER then begin
        // handle differently according to wParam and lParam
        // ...
      end;
    end;
  except
    on E: Exception do begin
      ...
    end;
  end;
end;

重要的是不要让异常得不到处理,因此所有内容都有一个顶级异常处理程序。你做的例外是你的选择,取决于应用程序,但必须捕获所有异常,否则应用程序将被终止。在服务中,您唯一的选择可能是记录它们。

有一种启动线程关闭的特殊方法,因为当线程在 GetMessage()中时需要被唤醒:

procedure TCustomTestThread.Shutdown;
begin
  Terminate;
  Cancel; // internal method dealing with worker objects used in thread
  DoSendMessage(WM_QUIT);
end;

procedure TCustomTestThread.DoSendMessage(AMessage: Cardinal;
  AWParam: integer = 0; ALParam: integer = 0);
begin
  PostThreadMessage(ThreadID, AMessage, AWParam, ALParam);
end;

发布 WM_QUIT 将导致消息循环退出。但是,后代类中的代码可能依赖于在线程关闭期间正确处理的Windows消息,尤其是在使用COM接口时。这就是为什么而不是简单的 WaitFor(),以下代码用于释放所有正在运行的线程:

procedure TCustomTestController.BeforeDestruction;
var
  i: integer;
  ThreadHandle: THandle;
  WaitRes: LongWord;
  Msg: TMsg;
begin
  inherited;
  for i := Low(fPositionThreads) to High(fPositionThreads) do begin
    if fPositionThreads[i] <> nil then try
      ThreadHandle := fPositionThreads[i].Handle;
      fPositionThreads[i].Shutdown;
      while TRUE do begin
        WaitRes := MsgWaitForMultipleObjects(1, ThreadHandle, FALSE, 30000,
          QS_POSTMESSAGE or QS_SENDMESSAGE);
        if WaitRes = WAIT_OBJECT_0 then begin
          FreeAndNil(fPositionThreads[i]);
          break;
        end;
        if WaitRes = WAIT_TIMEOUT then
          break;

        while PeekMessage(Msg, 0, 0, 0, PM_REMOVE) do begin
          TranslateMessage(Msg);
          DispatchMessage(Msg);
        end;
      end;
    except
      on E: Exception do
        // ...
    end;
    fPositionThreads[i] := nil;
  end;
end;

这是在重写的 BeforeDestruction()方法中,因为在后代控制器类的析构函数开始释放线程可能使用的任何对象之前,需要释放所有线程。

答案 1 :(得分:2)

我建议你考虑使用IPC的命名管道。这就是他们的目的:

Looking for an alternative to windows messages used in inter-process communication

答案 2 :(得分:2)

正如Mghie所说,你需要一个消息处理循环。这就是PeekMessage正确返回消息的原因。并不是消息不在那里,而是你没有处理它们。在标准应用程序中,Delphi创建一个TApplication类并调用Application.Run。这是普通应用程序的消息处理循环。它基本上包括:

  repeat
    try
      HandleMessage;
    except
      HandleException(Self);
    end;
  until Terminated;

如果您希望服务应用程序处理消息,则需要执行相同类型的工作。

有一个使用服务和处理PostThreadMessage调度here的示例。请记住,正如Mick所提到的,您不能在不同安全上下文的应用程序之间使用消息处理(特别是在Vista中)。您应该使用命名管道或类似的。 Microsoft讨论了这个here

编辑:

根据您发布的代码段,您可能只是在解决线程问题。 AllocHWnd不是线程安全的。有关该问题的详细解释以及在线程中正常运行的版本,请参阅here

当然,这仍然会让我们回到你不使用PostThreadMessage的原因。您的代码示例的结构方式,使消息处理线程的函数然后将其传递给类以进行处置将是微不足道的。

答案 3 :(得分:1)

感谢您的所有答案。我想我们可以忘记这个问题了。我创建了一个新的服务应用程序并执行了快速的消息后测试消息传递正确,所以我希望我现在可以说通常一切正常,只有我描述的这一项服务出错了。我知道这是愚蠢的,但我会尝试将一个接一个的代码片段从“坏”服务复制到一个新的。也许这会帮助我找到问题的原因。

我希望我现在可以认为消息等待循环是不必要的,只要没有它就一切正常,不是吗?

如果涉及到权限,微软表示:“UAC使用WIM来阻止在不同权限级别的进程之间发送Windows消息。”我的Vista的UAC已关闭,我没有为我描述的那些服务设置任何权限。除此之外,我不在不同进程之间发送消息。消息在一个进程中发送。

为了让您知道我在做什么,我将向您展示测试服务应用程序的代码片段。

uses ...;

type
  TMyThread = class;
  TMyClass = class
  private
    FThread: TMyThread;
    procedure ReadMessage(var Msg: TMessage);
  public
    FHandle: HWND;
    constructor Create;
    destructor Destroy; override;
  end;

  TMyThread = class(TThread)
  private
    FMyClass: TMyClass;
  protected
    procedure Execute; override;
    constructor Create(MyClass: TMyClass); reintroduce;
  end;

implementation

{ TMyClass }

constructor TMyClass.Create;
begin
  inherited Create;
  FHandle := AllocateHWnd(ReadMessage);
  FThread := TMyThread.Create(Self);
end;

destructor TMyClass.Destroy;
begin
  FThread.Terminate;
  FThread.WaitFor;
  FThread.Free;
  DeallocateHWnd(FHandle);
  inherited Destroy;
end;

procedure TMyClass.ReadMessage(var Msg: TMessage);
begin
  Log.Log('message read: ' + IntToStr(Msg.Msg));
end;

{ TMyThread }

constructor TMyThread.Create(MyClass: TMyClass);
begin
  inherited Create(True);
  FMyClass := MyClass;
  Resume;
end;

procedure TMyThread.Execute;
begin
  while not Terminated do
  begin
    //do some work and
    //send a message when finished
    if PostMessage(FMyClass.FHandle, WM_USER, 0, 0) then
      Log.Log('message sent')
    else
      Log.Log('message not sent: ' + SysErrorMessage(GetLastError));
    //do something else...
    Sleep(1000);
  end;
end;

这只是一个例子,但我的真实代码的功能基于相同的想法。当您创建此类的对象时,它将创建一个将开始向该类发送消息的线程。 Log.Log()将数据保存到文本文件中。当我在新的服务应用程序中使用此代码时,一切正常。当我把它放入“破损”的服务时,却没有。请注意,我不使用任何消息等待循环来接收消息。我创建了一个新服务,只需将上面的代码放入其中,然后创建该类的对象。就是这样。

如果我知道为什么这在“破损”服务中不起作用,我会写下来。

感谢您投入我的时间。

马里乌什。

答案 4 :(得分:0)

以下是我要尝试的内容:

  • 检查PostMessage的返回值和GetLastError
  • 这是Vista / 2008机器吗?如果是,请检查发送应用程序是否有足够的权限发送消息。

我必须有更多信息来帮助你。

答案 5 :(得分:0)

我花了很长时间试图找到没有收到邮件的原因。正如我在我的代码片段中所示,该类的构造函数创建了一个窗口句柄,我用它来发送消息。只要该类是由主线程构造的,一切正常的窗口句柄(如果我理解正确的话)存在于主线程的上下文中,默认情况下等待消息。在“破碎”服务中,正如我错误地调用它,我的类是由另一个线程创建的,因此句柄必须存在于该线程的上下文中。因此,当我使用此窗口句柄发送消息时,它们被该线程接收,而不是由主线程接收。由于此线程没有任何消息等待循环,因此根本没有收到我的消息。 我只是不知道它是这样工作的。为了以一种简单的方式解决问题,我在主线程中创建并销毁该类,即使我在第二个中使用它。

感谢您的时间和您给我的所有信息。

答案 6 :(得分:0)

Mghie,我认为你是对的。我用这种方式实现了一个消息等待循环:

procedure TAsyncSerialPort.Execute;
var
  Msg: tagMSG;
begin
  while GetMessage(Msg, 0, 0, 0) do
  begin
    {thread message}
    if Msg.hwnd = 0 then
    begin
      case Msg.message of
        WM_DATA_READ: Log.Log('data read');
        WM_READ_TIMEOUT: Log.Log('read timeout');
        WM_DATA_WRITTEN: Log.Log('data written');
        WM_COMM_ERROR: Log.Log('comm error');
      else
        DispatchMessage(Msg);
      end;
    end
    else
      DispatchMessage(Msg);
  end;
end;

我是第一次这样做,所以请你检查代码是否正确吗?实际上,这是我真正的类代码片段(日志将替换为真实的代码)。它处理重叠的通信端口。有两个线程将线程消息发送到上面的线程,通知它们从通信端口写入或接收了一些数据等。当线程得到这样的消息时,它需要一个动作 - 它从队列中获取接收到的数据,线程首先放置它然后调用外部方法,让我们说,分析接收到的数据。我不想详细说明它是不重要的:)。我发送这样的线程消息:PostThreadMessage(MyThreadId,WM_DATA_READ,0,0)。

这个代码在检查时正常工作,但我想确保一切正确,所以我问你这个问题。如果你的回答我将不胜感激。

要释放线程,请执行以下操作:

destructor TAsyncSerialPort.Destroy;
begin
  {send a quit message to the thread so that GetMessage returns false and the loop ends}
  PostThreadMessage(ThreadID, WM_QUIT, 0, 0);

  {terminate the thread but wait until it finishes before the following objects 
  (critical sections) are destroyed for the thread might use them before it quits}
  Terminate;
  if Suspended then
    Resume;
  WaitFor;

  FreeAndNil(FLock);
  FreeAndNil(FCallMethodsLock);
  inherited Destroy;
end;

我希望这是结束消息循环的正确方法。

非常感谢你的帮助。

顺便说一句,我希望我的英语是可以理解的,不是吗? :)对不起,如果你很难理解我。

答案 7 :(得分:0)

线程中的消息循环中有一个技巧。 Windows不会立即为线程创建消息队列,因此将某些时间发送到线程的消息将失败。详细信息为here。在我的msg循环线程中,我使用MS建议的技术:

constructor TMsgLoopThread.Create;
begin
  inherited Create(True);
  FEvMsgQueueReady := CreateEvent(nil, True, False, nil);
  if FEvMsgQueueReady = 0 then
    Error('CreateEvent: '+LastErrMsg);
end;

procedure TMsgLoopThread.Execute;
var
  MsgRec: TMsg;
begin
  // Call fake PeekMessage for OS to create message queue for the thread.
  // When it finishes, signal the event. In the main app execution will wait
  // for this event.
  PeekMessage(MsgRec, 0, WM_USER, WM_USER, PM_NOREMOVE);
  SetEvent(FEvMsgQueueReady);

  ...
end;

// Start the thread with waitinig for it to get ready
function TMsgLoopThread.Start(WaitInterval: DWORD): DWORD;
begin
  inherited Start;
  Result := WaitForSingleObject(FEvMsgQueueReady, WaitInterval);
end;

但在你的情况下,我强烈建议使用其他的IPC手段。