PostMessage в сервисных приложениях - PullRequest
1 голос
/ 24 февраля 2009

Существует проблема, которую я не могу решить. Я создал два служебных приложения в Delphi и пытался публиковать в них сообщения. Конечно, в таких приложениях нет окон, и PostMessage нужен параметр дескриптора окна для отправки сообщения.

Поэтому я создал дескриптор окна с помощью функции AllocateHWnd (MyMethod: TWndMethod) и передал в качестве параметра «MyMethod» процедуру, которую я хочу вызывать при получении сообщения. Если бы это было оконное приложение, PostMessage (), вызванный с использованием дескриптора, возвращенного методом AllocateHWnd, непременно отправил бы сообщение, которое затем было бы получено процедурой «MyMethod».

Однако в моих сервисных приложениях ситуация иная. Я не понимаю, почему, но в одном из них отправка сообщений таким способом работает нормально, а во втором - нет (сообщения не принимаются вообще). Только когда служба остановлена, я замечаю, что MyMethod получает два сообщения: WM_DESTROY и WM_NCDESTROY. Сообщения, которые я отправляю с помощью PostMessage, никогда не будут получены этой процедурой. С другой стороны, первая служба всегда получает все отправленные мной сообщения.

Не могли бы вы дать мне подсказку, которая поможет мне найти причину, по которой вторая служба не получает мои сообщения? Я не знаю, чем они могут отличаться. Я проверил настройки служб, и они кажутся идентичными. Почему тогда один из них работает нормально, а второй - нет (что касается отправки сообщений)?

Спасибо за любой совет. Мариуш.

Ответы [ 8 ]

3 голосов
/ 24 февраля 2009

Без дополнительной информации вам будет трудно помочь отладить это, особенно почему это работает в одном сервисе, но не в другом. Тем не менее:

Вместо того, чтобы пытаться исправить проблему в вашем коде, вы можете удалить окна полностью и использовать PostThreadMessage () вместо PostMessage (). Для корректной отправки сообщений вам необходим цикл сообщений, но не обязательно получение окон.

Редактировать: Я пытаюсь ответить на все ваши ответы за один раз.

Во-первых, если вы хотите облегчить свою жизнь, вам действительно стоит проверить OmniThreadLibrary от gabr . Я не знаю, работает ли он в приложении-службе Windows, я даже не знаю, было ли это опробовано. Вы можете спросить на форуме. Однако он обладает множеством замечательных функций и заслуживает внимания, хотя бы для изучения эффекта.

Но, конечно, вы также можете запрограммировать это для себя, и вам придется сделать это для версий Delphi до Delphi 2007. Я просто добавлю некоторые фрагменты из нашей внутренней библиотеки, которая развивалась годами и работает в нескольких десятках программ. , Я не утверждаю, что это без ошибок, хотя. Вы можете сравнить его со своим кодом, и, если что-то не получится, не стесняйтесь спрашивать, и я постараюсь уточнить.

Это упрощенный метод 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 () , потому что все потоки должны быть освобождены, прежде чем деструктор класса контроллера-потомка начнет освобождать любые объекты, которые потоки могут использовать.

2 голосов
/ 24 февраля 2009

Как уже упоминал Мги, вам нужен цикл обработки сообщений. Вот почему PeekMessage возвращает сообщения правильно. Дело не в том, что сообщений нет, а в том, что вы их не обрабатываете. В стандартном приложении Delphi создает класс TApplication и вызывает Application.Run. Это цикл обработки сообщений для обычного приложения. В основном состоит из:

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

Если вы хотите, чтобы приложение-служба обрабатывало сообщения, вам необходимо выполнить такую ​​же работу.

Существует пример использования службы и обработки сообщений PostThreadMessage здесь . Имейте в виду, как упоминал Мик, вы не можете использовать обработку сообщений между приложениями с различным контекстом безопасности (особенно в Vista). Вы должны использовать именованные каналы или аналогичные. Microsoft обсуждает это здесь .

Edit:

Судя по фрагменту кода, который вы разместили, вы можете просто бороться с проблемой многопоточности. AllocHWnd не является потокобезопасным. См. здесь для действительно подробного объяснения проблемы и версии, которая работает правильно в потоках.

Конечно, это все еще возвращает нас к тому, почему вы вместо этого не используете PostThreadMessage. То, как ваш пример кода структурирован, было бы тривиально сделать обработку сообщения функцией потока, а затем передать его в класс для удаления.

2 голосов
/ 24 февраля 2009

Я бы посоветовал вам рассмотреть возможность использования именованных каналов для IPC. Вот для чего они предназначены:

Поиск альтернативы сообщениям Windows, используемым в межпроцессном взаимодействии

1 голос
/ 24 февраля 2009

Спасибо за все ваши ответы. Я думаю, что мы можем забыть о проблеме. Я создал новое сервисное приложение и провел быстрые тесты после сообщения. Сообщения были доставлены правильно, поэтому я надеюсь, что теперь я могу утверждать, что обычно все работает нормально, и что-то не так только с этой службой, которую я описал. Я знаю, что это глупо, но я просто попытаюсь скопировать один фрагмент кода за другим из «плохого» сервиса в новый. Может быть, это поможет мне найти причину проблемы.

Надеюсь, теперь я могу считать цикл ожидания сообщений ненужным, если все работает без него, не так ли?

Если речь идет о привилегиях, Microsoft говорит: «UAC использует WIM для блокировки отправки сообщений Windows между процессами с разными уровнями привилегий». UAC моей Vista выключен, и я не установил никаких привилегий для тех служб, которые я описал. Кроме того, я не отправляю сообщения между разными процессами. Сообщения отправляются в течение одного процесса.

Чтобы дать вам представление о том, что я делаю, я покажу вам фрагмент кода из приложения-службы тестирования.

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 () сохраняет данные в текстовый файл. Когда я использую этот код в новом сервисном приложении, все работает нормально. Когда я помещаю это в 'сломанный' сервис, это не делает. Обратите внимание, что я не использую цикл ожидания сообщений для получения сообщений. Я создал новый сервис и просто поместил в него код выше, а затем создал объект класса. Вот и все.

Если я узнаю, почему это не работает в «сломанной» службе, я напишу об этом.

Спасибо, что уделили мне время.

Мариуш.

0 голосов
/ 24 марта 2016

Есть одна хитрость в циклах сообщений в темах. Windows не создаст очередь сообщений для потока немедленно, поэтому будет некоторое время, когда отправка сообщений в поток не удастся. Подробности здесь . В моей теме цикла сообщений я использую технику, которую предлагает 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.

0 голосов
/ 25 февраля 2009

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;

Я надеюсь, что это правильный способ завершить цикл обработки сообщений.

Большое спасибо за вашу помощь.

Кстати, я надеюсь, что мой английский язык понятен, не так ли? :) Извините, если у вас есть трудности с пониманием меня.

0 голосов
/ 25 февраля 2009

Я потратил много часов, пытаясь выяснить причину получения сообщений. Как я показал в своем фрагменте кода, конструктор класса создает дескриптор окна, в который я отправлял сообщения. Пока класс создавался основным потоком, все работало нормально, поскольку дескриптор окна (если я правильно понимаю) существовал в контексте основного потока, который по умолчанию ожидает сообщения. В «сломанном» сервисе, как я его назвал по ошибке, мой класс был создан другим потоком, поэтому дескриптор должен существовать в контексте этого потока. Поэтому, когда я отправлял сообщения, используя этот дескриптор окна, они были получены этим потоком, а не основным. Из-за того, что в этом потоке не было цикла ожидания сообщений, мои сообщения вообще не были получены. Я просто не знал, что так получилось. Чтобы решить проблему простым способом, я создаю и уничтожаю класс в главном потоке, хотя я использую его во втором.

Спасибо за ваше время и всю информацию, которую вы мне дали.

0 голосов
/ 24 февраля 2009

Вот что я бы попробовал:

  • Проверьте возвращаемое значение и GetLastError PostMessage
  • Это машина Vista / 2008? Если да, проверьте, достаточно ли у отправляющего приложения прав для отправки сообщения.

Мне нужно больше информации, чтобы помочь вам в дальнейшем.

...