Thursday, 6 August 2009

Manejador de excepciones avanzadas con Delphi

El otro día comentaba con uno de los compañeros de trabajo que sería interesante poder disponer de la información de la pila (Call Stack) en tiempo de ejecución, y poder visualizar la información del error con más información, es decir con la información de la pila para saber de dónde procedía el error y así seguirlo con facilidad. Hay que ser pragmáticos con éste tema y evitar hacer logs inmensos de seguimiento simplemente porqué no encontramos el error. De ésta manera podríamos encontrar el error rápidamente, analizando la Call Stack. Si os fijáis en la Call Stack de delphi, cuando ejecutamos una aplicación, y ponemos un punto de interrupción, podemos ver por dónde va pasando el programa y los métodos que va ejecutando para llegar hasta nuestro punto de interrupción. En la siguiente imágen podéis ver una ejecución simple de una aplicación y su paro en un break point.


Para mi ejemplo haré reposting, y utilizaré el ejemplo que escribió Sergey Shirokov y Alex P en Advanced Exception Handler. En su artículo comentan concretamente lo que yo quiero obtener de la pila. Para la obtención de los datos avanzados, necesitaré el siguiente componente: ExWatcher de la mano de Tomek Guz (Solo la versión para Delphi 5). Además utilizaremos el proyecto demo, llamado ExWatcherDemo.
  • Instalando el componente ExWatcher
Primero, lo que haremos será instalar el componente ExWatcher. Lo he probado con Delphi 2007 y 2009 y funciona perfectamente, sin ningún problema. Descomprimimos los ficheros de ExWatcher y los ponemos en una ubicación típica (dónde tengamos los diferentes componentes).

Ahora compilamos, hacemos build e Install. Una vez instalado el componente nos aparecerá el siguiente mensaje:

Ahora una vez instalado el componente, tenemos que añadir la ruta dónde están los .dcu en la library path de delphi. Ésto se hace iéndo a -> Tools -> Options y en el nodo Environment Options -> Delphi Options -> Library -Win32, añadimos la ruta de la carpeta del ExWatcher en Library Path.
  • Ejecutando la demo
Ahora que ya tenemos el componente instalado, abrimos el proyecto demo ExWatcherDemo.dpr y veremos que nos aparece el componente ExWatcher.

He modificado el TMemo y lo he hecho un poco más grande y he modificado también los colores. Ahora una vez tenemos el proyecto OK, tenemos que modificar los siguientes parámetros para su compilación:

Marcamos las opciones de compilación : Stack Frames y de Linkado : Map File -> Publics. De ésta manera creamos los ficheros necesarios que nos darán la información de los valores de la pila en tiempo de ejecución.
  • map2dbg
Ahora ya podemos realizar la ejecución de nuestra aplicación, pero antes, tenemos que realizar una pequeña modificación a nuestro proyecto para que las llamadas de la Call Stack se puedan visualizar. Para ello, tenemos que utilizar la aplicación map2dbg de la mano de Lucian Wischik, que permite convertir los ficheros .map en ficheros .dbg. De ésta manera convertimos los ficheros de debug de Delphi a ficheros que entiende windows mediante su estándard .dbg. Para ello, descargaremos la última versión de la aplicación de map2dbg: map2dbg v1.3.

Una vez descargada la aplicación, la copiamos dentro de nuestra aplicación demo, y ejecutamos lo siguiente desde el intérprete de comandos:

map2dbg.exe exwatcherdemo.exe

y nos aparecerá el siguiente resultado:

converted 4150 symbols

Ahora, si ejecutamos el ejemplo y lanzamos alguna excepción obtendremos todas las llamadas de la Call Stack en nuestro TMemo:

Como podéis ver, no tiene desperdicio. Ahora ya podemos crear aplicaciones con mayor control sobre los errores. Espero que os haya servido de ayuda!.

Aquí os dejo con el proyecto de prueba que he utilizado: ExWatcherDemo Project.rar. Y el código fuente de la unidad principal para que veáis lo fácil que es utilizar el componente:

unit main;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, clExWatcher, ShowExcept;

type
  TMainForm = class(TForm)
    btnDelphiException: TButton;
    btnDelphiSafeCallException: TButton;
    btnSystemException: TButton;
    clExceptWatcher: TclExceptWatcher;
    btnSystemSafeCallException: TButton;
    btnTryExcept: TButton;
    memStackLog: TMemo;
    procedure clExceptWatcherDelphiException(Sender: TObject; E: Exception;
      EI: TclExceptionInformation; StackTracer: TclStackTracer);
    procedure clExceptWatcherDelphiSafeCallException(Sender,
      Target: TObject; E: Exception; EI: TclExceptionInformation;
      StackTracer: TclStackTracer);
    procedure clExceptWatcherSystemException(Sender: TObject;
      EI: TclExceptionInformation; StackTracer: TclStackTracer);
    procedure clExceptWatcherSystemSafeCallException(Sender,
      Target: TObject; EI: TclExceptionInformation;
      var NotifyTarget: Boolean; StackTracer: TclStackTracer);
    procedure btnDelphiExceptionClick(Sender: TObject);
    procedure btnDelphiSafeCallExceptionClick(Sender: TObject);
    procedure btnSystemExceptionClick(Sender: TObject);
    procedure btnSystemSafeCallExceptionClick(Sender: TObject);
    procedure btnTryExceptClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    procedure SimulateDelphiException();
    procedure SimulateSystemException();
    procedure SimulateDelphiSafeCallException(); safecall;
    procedure SimulateSystemSafeCallException(); safecall;
    procedure ShowStack(Frames: TclStackFrames);
    procedure ExceptionHandler(Sender: TObject; E: Exception);
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

procedure TMainForm.clExceptWatcherDelphiException(Sender: TObject;
  E: Exception; EI: TclExceptionInformation; StackTracer: TclStackTracer);
begin
  memStackLog.Clear();
  memStackLog.Lines.Add('Delphi Exception:');
  ShowStack(StackTracer.GetCallStack(EI));
end;

procedure TMainForm.clExceptWatcherSystemException(Sender: TObject;
  EI: TclExceptionInformation; StackTracer: TclStackTracer);
begin
  memStackLog.Clear();
  memStackLog.Lines.Add('System Exception:');
  ShowStack(StackTracer.GetCallStack(EI));
end;

procedure TMainForm.clExceptWatcherDelphiSafeCallException(Sender,
  Target: TObject; E: Exception; EI: TclExceptionInformation;
  StackTracer: TclStackTracer);
begin
  memStackLog.Clear();
  memStackLog.Lines.Add('Delphi SafeCall Exception:');
  ShowStack(StackTracer.GetCallStack(EI));
end;

procedure TMainForm.clExceptWatcherSystemSafeCallException(Sender,
  Target: TObject; EI: TclExceptionInformation; var NotifyTarget: Boolean;
  StackTracer: TclStackTracer);
begin
  memStackLog.Clear();
  memStackLog.Lines.Add('System SafeCall Exception:');
  ShowStack(StackTracer.GetCallStack(EI));
end;

procedure TMainForm.btnDelphiExceptionClick(Sender: TObject);
begin
  SimulateDelphiException();
end;

procedure TMainForm.btnSystemExceptionClick(Sender: TObject);
begin
  SimulateSystemException();
end;

procedure TMainForm.btnDelphiSafeCallExceptionClick(Sender: TObject);
begin
  SimulateDelphiSafeCallException();
end;

procedure TMainForm.btnSystemSafeCallExceptionClick(Sender: TObject);
begin
  SimulateSystemSafeCallException();
end;

procedure TMainForm.btnTryExceptClick(Sender: TObject);
begin
  try
    try
      SimulateSystemException();
    except
      raise;
    end;
  except
  end;
end;

procedure TMainForm.SimulateDelphiException;
begin
  raise Exception.Create('Test Delphi exception');
end;

procedure TMainForm.SimulateSystemException;
var
  p: PChar;
begin
  p := PChar(5);
  p^ := #9; //Access Violation here
end;

procedure TMainForm.SimulateDelphiSafeCallException;
begin
  raise Exception.Create('Test Delphi SafeCall exception');
end;

procedure TMainForm.SimulateSystemSafeCallException;
var
  p: PChar;
begin
  p := PChar(5);
  p^ := #9; //Access Violation here
end;

procedure TMainForm.ShowStack(Frames: TclStackFrames);
var
  i: Integer;
begin
  memStackLog.Lines.BeginUpdate();
  for i := 0 to Frames.Count - 1 do
  begin
    memStackLog.Lines.Add(Format('%x: %s - %s', [Integer(Frames[i].CallAddress),
      ExtractFileName(Frames[i].ModuleName), Frames[i].FunctionName]));
  end;
  memStackLog.Lines.EndUpdate();
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  Application.OnException := ExceptionHandler;
end;

procedure TMainForm.ExceptionHandler(Sender: TObject; E: Exception);
begin
  ShowExceptionForm(E.Message);
end;

end.

  • Enlaces de interés:
Delphi Crash - Exception Handling with bug reporting.
Madshi.net.
EurekaLog.

4 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Hi there Jordi! It seems to be an absolutely great component. But, woah! @#$**)!, it doesn't work for me on Vista x64! It just appears the last line, none of the call stack. At first I thought it was my fault by installation problems. Then, I had a try putting the same .exe on a winXP x32 machine, and it perfectly worked! Vista seems to be having fun with us... Can anyone have a try on Vista x32?
    BTW, thank you for the post Jordi!

    ReplyDelete
  3. Hi!, that's the problem with de .dbg information, it only works with 32 bits. Don't worry about that, I'll take a look. Thank you for your comment!

    ReplyDelete