delphiexceptiondelphi-xe2jedi-code-library

Why program crashes even when caught by a global exception hook?


Problem Summary: Some code in UartComm.OnGetIdRES() raises an ERangeError, which crashes my program. This bug isn't the problem, what matters is why my application-global exception hook catches the exception, but my program still crashes. I expect the hook to catch all unhandled exceptions and suppress them; the program should keep running.

Here is the unit responsible for the global exception hook:

unit LogExceptions;

interface
uses
  Windows, SysUtils, Classes, JclDebug, JclHookExcept;

procedure AppendToLog(Msg: String; const LogFileLevel: TLogFileLevel);

implementation
uses Main;

procedure HookGlobalException(ExceptObj: TObject; ExceptAddr: Pointer;
                              OSException: Boolean);
var
  Trace: TStringList;
  DlgErrMsg: String;
begin
  { Write stack trace to `error.log`. }
  Trace := TStringList.Create;
  try
    Trace.Add(
        Format('{ Original Exception - %s }', [Exception(ExceptObj).Message]));
    JclLastExceptStackListToStrings(Trace, False, True, True, False);
    Trace.Add('{ _______End of the exception stact trace block_______ }');
    Trace.Add(' ');

    Trace.LineBreak := sLineBreak;
    LogExceptions.AppendToLog(Trace.Text, lflError);

    { Show an dialog to the user to let them know an error occured. }
    DlgErrMsg := Trace[0] + sLineBreak +
      Trace[1] + sLineBreak +
      sLineBreak +
      'An error has occured. Please check "error.log" for the full stack trace.';
    frmMain.ShowErrDlg(DlgErrMsg);
  finally
    Trace.Free;
  end;
end;

procedure AppendToLog(Msg: String; const LogFileLevel: TLogFileLevel);
{ .... irrelevant code ....}

initialization
  Include(JclStackTrackingOptions, stTraceAllExceptions);
  Include(JclStackTrackingOptions, stRawMode);

  // Initialize Exception tracking
  JclStartExceptionTracking;

  JclAddExceptNotifier(HookGlobalException, npFirstChain);
  JclHookExceptions;

finalization
  JclUnhookExceptions;
  JclStopExceptionTracking;

end.

(If it's helpful here's a link to JclDebug.pas and JclHookExcept.pas)

All I do to activate the hook is to add LogExceptions to the interface uses list in Main.pas.

Now here is a step-by-step of the crash:

  1. Execution enters UartComm.OnGetIdRES()
  2. ERangeError is raised when I try to set the Length of a dynamic array to -7:

    SetLength(InfoBytes, InfoLength);

  3. We enter LogExceptions.HookGlobalException(). The call stack shown in the IDE at this moment is this (I left out memory addresses):

    ->  LogExceptions.HookGlobalException
        :TNotifierItem.DoNotify
        :DoExceptNotify
        :HookedRaiseException
        :DynArraySetLength
        :DynArraySetLength
        :@DynArraySetLength
        UartComm.TfrmUartComm.OnSpecificRES // This method runs `OnGetIdRES()`
        UartComm.TfrmUartComm.OnSpecificPktRX
        UartComm.TfrmUartComm.DisplayUartFrame
        UartComm.TfrmUartComm.UartVaComm1RxChar
        VaComm.TVaCustommComm.HandleDataEvent
        VaComm.TVaCommEventThread.DoEvent
        { ... }
        { ... Some low-level calls here .... }
    
  4. As soon we come out of HookGlobalException the debugger throws a dialog:

    raised exception class ERangeError with message 'Range check error'

If I press "Continue" program is still frozen work. Without the debugger the program also freezes at this point.

  1. If I click "Break" and keep stepping with the debugger, execution falls through the stack all the way into VaComm.TVaCommEventThread.DoEvent and executes the line:

    Application.HandleException(Self);
    

After which it does nothing (I stepped into this routine with the debugger and program is "running" forever).

Even if I don't use the JCL library for the hook, and instead point Application.OnException to some empty routine, the exact same thing happens.

Why is the exception caught by the hook and then re-raised when the hook returns? How can I suppress the exception so that the program doesn't crash but keeps running?

UPDATE: I made 3 great discoveries:

I'll write an answer when I figure everything out.


Solution

  • The problem was that UartComm.TfrmUartComm.UartVaComm1RxChar was event triggered. When an unhandled exception was raised in this routine, execution fell through the call stack until it reached Application.OnException.

    Inside OnException I tried to close the COM port with VaComm1.Close(). Part of Close() was a call to stop the VaComm1 thread and WaitFor() the thread to finish. But remember that the UartVaComm1RxChar never returned! The never finished! So this WaitFor() is waiting forever.

    The solution was to enable a TTimer inside OnException and move the VaComm1.Close() routine inside this Timer. The program finished handling the raised exception and returned back to executing the "main" loop, with the event finished. Now the TTimer fires and closes the COM port.

    More details here.