Искусство управления ошибками
  
  Автор: Даутов Ильдар 
Часть IIПродолжая тему "Управление ошибками в 
Delphi", поставим следующие задачи : 
  - программа-монитор ошибок должна работать как системный сервис Windows NT 
  
 - журнал ошибок должен сохраняться на диске и постоянно пополняться 
  
 - список текущих ошибок и полный журнал ошибок должны быть доступны для 
  просмотра на любом компьютере локальной сети предприятия 
  Реализуем 
следующую схему взаимодействия программ при возникновении ошибки : 
  - ошибка, возникшая в клиентской программе, передается по сети 
  монитору-сервису Windows NT. Для передачи используем механизм каналов Mailslot 
  
 - монитор сохраняет текст ошибки на диске. Для хранения используем текстовый 
  файл 
  
 - монитор пересылает по сети текст ошибки программе просмотра ошибок. Для 
  передачи используем механизм каналов Mailslot 
  
 - программа просмотра принимает текст ошибки и отображает его на экране 
  
 - программа просмотра может запросить полный журнал ошибок. Для получения 
  полного журнала используем механизм разделяемых сетевых файловых ресурсов 
  В статье представлены 2 проекта : монитор ошибок и окно просмотра 
ошибок. Клиентская программа, имитирующая ошибку, была представлена в предыдущей статье, 
и здесь не рассматривается. 
Монитор ошибок 
Оформить программу как сервис Windows NT (Win32 service) не составляет 
большого труда : 
  - создаем новое приложение File | New... | New | Service Application. 
  Создается приложение с глобальной переменной Application типа 
  TServiceApplication и объектом типа TService, который и реализует всю 
  функциональность сервиса 
  
 - устанавливаем требуемые свойства объекта TService 
  
    - имя сервиса 
    
 - параметры запуска сервиса 
    
 - имя и пароль пользователя, от имени которого стартует сервис 
  
   - переписываем событие OnExecute объекта TService, в котором реализуем 
  требуемую функциональность сервиса 
  
 - компилируем проект 
  
 - регистрируем созданный сервис на сервере Windows NT и запускаем 
  Регистрация сервиса выполняется из командной строки следующим образом 
: ErrorMonitorService.exe /install  Удаление сервиса 
: ErrorMonitorService.exe /uninstall  Запуск сервиса выполняется из 
командной строки следующим образом : net start ErrorMonitor  Останов 
сервиса : net stop ErrorMonitor 
  Оформив эту последовательность команд 
как BAT-файл, можно значительно облегчить себе жизнь при отладке 
сервиса.
  Достаточно подробную информацию о сервисах Windows NT можно 
найти в книге : А.В.Фролов, Г.В.Фролов 'Программирование для Windows NT (часть 
вторая)', Москва, ДИАЛОГ-МИФИ, 1997
  Для сохранения протокола (журнала) 
пользовательских ошибок используем следующую схему : 
  - журнал ведется в текстовом файле в определенном каталоге Windows NT 
  
 - журнал имеет имя yyyy-mm-dd.log, соответствующее календарной дате запуска 
  сервера 
  
 - при каждом запуске монитор проверяет наличие файла, имя которого 
  соответствует текущей дате. При отсутствии - файл создается, иначе происходит 
  дозапись в конец файла 
  
 - сохраняются только последние 7 файлов журнала 
  Текст программы 
монитора ошибок приведен ниже : 
unit uErrorMonitorService;
interface
uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, SvcMgr, ScktComp;
type
  TErrorMonitor = class(TService)
    procedure Service1Execute(Sender: TService);
    procedure ServiceEMCreate(Sender: TObject);
  private
  public
    function GetServiceController: PServiceController; override;
    procedure SendError;
    function InitLog: boolean;
  end;
var
  ErrorMonitor: TErrorMonitor;
implementation
uses Dialogs;
{$R *.DFM}
const
  LogDir = 'C:\Log\'; // каталог, где сохраняются журналы
var
  LogFile: TextFile; // файл текущего журнала
  LogName: string; // имя файла текущего журнала
  h: THandle; // handle канала Mailslot
  str: string[250]; // буфер для передачи информации
  MsgNumber, MsgNext, Read: DWORD;
procedure ServiceController(CtrlCode: DWord); stdcall;
begin
  ErrorMonitor.Controller(CtrlCode);
end;
function TErrorMonitor.GetServiceController: PServiceController;
begin
  Result := @ServiceController;
end;
// Передача текста ошибки от сервиса программе просмотра
procedure TErrorMonitor.SendError;
var
  h: THandle;
  i: integer;
begin
  // открытие MailSlot-канала, по которому будет передаваться протокол
  // используется широковещательная передача в домене
  h := CreateFile(PChar('\\*\mailslot\EMonMess'), GENERIC_WRITE,
    FILE_SHARE_READ, nil,
    OPEN_EXISTING, 0, 0);
  if h <> INVALID_HANDLE_VALUE then
  begin
    // запись в канал и закрытие канала
    WriteFile(h, str, Length(str) + 1, DWORD(i), nil);
    CloseHandle(h);
  end;
end;
// инициализация файла журнала
// журналы ведутся в отдельных файлах по каждой дате
function TErrorMonitor.InitLog: boolean;
var
  sr: TSearchRec;
  i: integer;
begin
  Result := True;
  // удаление старых файлов журнала
  //(сохраняются только последние 7 журналов)
  with TStringList.Create do
  begin
    Sorted := True;
    i := FindFirst(LogDir + '*.log', faAnyFile, sr);
    while i = 0 do
    begin
      Add(sr.Name);
      i := FindNext(sr);
    end;
    FindClose(sr);
    if Count > 7 then
      for i := 0 to Count - 8 do
        DeleteFile(LogDir + Strings[i]);
    Free;
  end;
  // текущий файл журнала
  LogName := LogDir + FormatDateTime('yyyy-mm-dd', Date) + '.log';
  AssignFile(LogFile, LogName);
  try
    if FileExists(LogName) then
      Append(LogFile)
    else
      Rewrite(LogFile);
  except
    str := 'Ошибка создания файла журнала : ' + LogName;
    Status := csStopped;
    LogMessage(str);
    ShowMessage(str);
    Result := False;
  end;
end;
// основная логика сервиса
procedure TErrorMonitor.Service1Execute(Sender: TService);
begin
  // создание MailSlot-канала с именем EMon - по этому имени к нему
  // будут обращаться клиенты, у которых возникли ошибки
  h := CreateMailSlot('\\.\mailslot\EMon', 0, MAILSLOT_WAIT_FOREVER, nil);
  if h = INVALID_HANDLE_VALUE then
  begin
    Status := csStopped;
    // запись в журнал событий NT
    str := 'Ошибка создания канала EMon !';
    LogMessage(str);
    ShowMessage(str);
    Exit;
  end;
  // создание файла журнала
  if not InitLog then
    Exit;
  try
    while not Terminated do
    begin
      // определение наличия сообщения в канале
      if not GetMailSlotInfo(h, nil, DWORD(MsgNext), @MsgNumber, nil) then
      begin
        Status := csStopped;
        str := 'Ошибка сбора информации канала EMon !';
        LogMessage(str);
        ShowMessage(str);
        Break;
      end;
      if MsgNext <> MAILSLOT_NO_MESSAGE then
      begin
        beep;
        // чтение сообщения из канала и добавление в текст протокола
        if ReadFile(h, str, 200, DWORD(Read), nil) then
        begin
          // запись в журнал
          Writeln(LogFile, str);
          // посылка сообщения для показа
          SendError;
        end
        else
        begin
          str := 'Ошибка чтения сообщения !';
          Writeln(LogFile, str);
          SendError;
        end;
        Flush(LogFile);
      end;
      sleep(500);
      ServiceThread.ProcessRequests(False);
    end;
  finally
    CloseHandle(h);
    CloseFile(LogFile);
  end;
end;
procedure TErrorMonitor.ServiceEMCreate(Sender: TObject);
begin
  // под таким именем наш сервис будет виден в Service Control Manager
  DisplayName := 'ErrorMonitor';
  // необходимо при использовании ShowMessage
  InterActive := True;
end;
end.
Окно просмотра ошибок  
  
Текст программы окна просмотра ошибок приведен ниже : 
unit fErrorMonitorMessage;
interface
uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, ExtCtrls, ScktComp;
type
  TfmErrorMonitorMessage = class(TForm)
    // протокол текущих ошибок
    meErrorTextNow: TMemo;
    meJournals: TMemo;
    // таймер для опроса канала
    Timer: TTimer;
    paJournals: TPanel;
    buJournals: TButton;
    lbJournals: TListBox;
    laJournals: TLabel;
    procedure FormCreate(Sender: TObject);
    procedure TimerTimer(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure buJournalsClick(Sender: TObject);
  private
  public
  end;
  // сетевой разделяемый ресурс, где сохраняются журналы
  // (укажите здесь имя своего ресурса и обеспечьте права для доступа)
const
  LogDir = '\\MyServer\C$\Log\';
var
  fmErrorMonitorMessage: TfmErrorMonitorMessage;
  h: THandle; // handle Mailslot-канала
  str: string[250]; // буфер обмена
  MsgNumber, MsgNext, Read: DWORD;
implementation
{$R *.DFM}
procedure TfmErrorMonitorMessage.FormCreate(Sender: TObject);
var
  sr: TSearchRec;
  i: integer;
begin
  // создание Mailslot-канала с именем EMonMess
  // по этому каналу будем получать сообщения об ошибках от сервиса NT
  h := CreateMailSlot('\\.\mailslot\EMonMess', 0, MAILSLOT_WAIT_FOREVER, nil);
  if h = INVALID_HANDLE_VALUE then
  begin
    ShowMessage('Ошибка создания канала !');
    Halt;
  end;
  // интервал опроса канала Mailslot - 3 секунды
  Timer.Interval := 3000;
  // таймер первоначально был выключен
  Timer.Enabled := True;
  // заполнение списка доступных журналов
  i := FindFirst(LogDir + '*.log', faAnyFile, sr);
  while i = 0 do
  begin
    lbJournals.Items.Add(sr.Name);
    i := FindNext(sr);
  end;
  lbJournals.ItemIndex := lbJournals.Items.Count - 1;
  FindClose(sr);
end;
procedure TfmErrorMonitorMessage.TimerTimer(Sender: TObject);
var
  str: string[250];
begin
  Timer.Enabled := False;
  // определение наличия сообщения в канале
  if not GetMailSlotInfo(h, nil, DWORD(MsgNext), @MsgNumber, nil) then
  begin
    ShowMessage('Ошибка сбора информации !');
    Close;
  end;
  if MsgNext <> MAILSLOT_NO_MESSAGE then
  begin
    beep;
    // чтение сообщения из канала и добавление в текст протокола
    if ReadFile(h, str, 200, DWORD(Read), nil) then
      meErrorTextNow.Lines.Add(str)
    else
      ShowMessage('Ошибка чтения сообщения !');
  end;
  Timer.Enabled := True;
end;
procedure TfmErrorMonitorMessage.FormClose(Sender: TObject;
  var Action: TCloseAction);
begin
  CloseHandle(h);
end;
procedure TfmErrorMonitorMessage.buJournalsClick(Sender: TObject);
var
  Journal: TFileStream;
  s: string;
begin
  // получение журнала ошибок за дату
  meJournals.Lines.Clear;
  meJournals.Lines.Add('Файл журнала ' +
    lbJournals.Items[lbJournals.ItemIndex]);
  Journal := TFileStream.Create(LogDir + lbJournals.Items[lbJournals.ItemIndex],
    fmOpenRead or fmShareDenyNone);
  SetLength(s, Journal.Size);
  Journal.Read(PChar(s)^, Journal.Size);
  meJournals.Lines.Add(s);
  Journal.Free;
end;
end.
 
           |