unit uExceptions;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils; 

function ExceptionToString: String;
procedure WriteExceptionToFile(const aFileName: String; const ExceptionText: String = '');
procedure WriteExceptionToErrorFile(const ExceptionText: String = ''); inline;
procedure ShowExceptionDialog(const ExceptionText: String = '');
procedure ShowException(e: Exception);
{en
   Log exception to file, show on console and show message dialog.
   Can be called from other threads.
}
procedure HandleException(e: Exception; AThread: TThread = nil);

implementation

uses
  Forms, Controls, Dialogs, LCLProc, LCLStrConsts, syncobjs,
  uDebug, uLng, uGlobs, uDCVersion, DCOSUtils, LazUTF8, DCConvertEncoding;

type
  THandleException = class
  private
    FHandleExceptionLock: TCriticalSection;
    FHandleExceptionMessage: String;
    FHandleExceptionBackTrace: String;
    procedure ShowException;
  public
    constructor Create; reintroduce;
    destructor Destroy; override;
    procedure HandleException(e: Exception; AThread: TThread = nil);
  end;

var
  HandleExceptionObj: THandleException;

function ExceptionToString: String;
var
  FrameCount: Integer;
  FrameNumber: Integer;
  Frames: PPointer;
begin
  Result := 'Unhandled exception:';

  if Assigned(ExceptObject) and (ExceptObject is Exception) then
  begin
    Result := Result + ' ' +
              Exception(ExceptObject).ClassName + ': ' +
              Exception(ExceptObject).Message;
  end;

  Result := Result + LineEnding +
            '  Stack trace:' + LineEnding +
            BackTraceStrFunc(ExceptAddr) + LineEnding;

  FrameCount := ExceptFrameCount;
  Frames := ExceptFrames;
  for FrameNumber := 0 to FrameCount - 1 do
    Result := Result + BackTraceStrFunc(Frames[FrameNumber]) + LineEnding;
end;

procedure WriteExceptionToFile(const aFileName: String; const ExceptionText: String);
var
  f: System.Text;
begin
  if (aFileName <> EmptyStr) and not mbDirectoryExists(aFileName) then
  begin
    AssignFile(f, UTF8ToSys(aFileName));
    {$PUSH}{$I-}
    if not mbFileExists(aFileName) then
      Rewrite(f)
    else if mbFileAccess(aFileName, fmOpenWrite or fmShareDenyNone) then
      Append(f);
    {$POP}
    if (TextRec(f).mode <> fmClosed) and (IOResult = 0) then
    begin
      WriteLn(f, '--------------- ',
                 FormatDateTime('dd-mm-yyyy, hh:nn:ss', SysUtils.Now),
                 ' ---------------');
      WriteLn(f, '| DC v', dcVersion, ' Rev. ', dcRevision,
                 ' -- ', TargetCPU + '-' + TargetOS + '-' + TargetWS);
      if WSVersion <> EmptyStr then
        Write(f, '| ', OSVersion, ' -- ', WSVersion)
      else
        Write(f, '| ', OSVersion);
      WriteLn(f, ' | PID ', GetProcessID);

      if ExceptionText = EmptyStr then
      begin
        if Assigned(ExceptObject) and (ExceptObject is Exception) then
          WriteLn(f, 'Unhandled exception: ',
                     Exception(ExceptObject).ClassName, ': ',
                     Exception(ExceptObject).Message)
        else
          WriteLn(f, 'Unhandled exception');
        WriteLn(f, '  Stack trace:');

        System.DumpExceptionBackTrace(f);
      end
      else
        WriteLn(f, ExceptionText);

      // Make one empty line.
      WriteLn(f);

      CloseFile(f);
    end;
  end;
end;

procedure WriteExceptionToErrorFile(const ExceptionText: String = '');
begin
  WriteExceptionToFile(gErrorFile, ExceptionText);
end;

procedure ShowExceptionDialog(const ExceptionText: String = '');
// Based on TApplication.ShowException.
var
  Msg: string;
  MsgResult: Integer;
begin
  if AppNoExceptionMessages in Application.Flags then exit;

  if ExceptionText = EmptyStr then
  begin
    if Assigned(ExceptObject) and (ExceptObject is Exception) then
      Msg := Exception(ExceptObject).Message
    else
      Msg := '';
  end
  else
    Msg := ExceptionText;

  if FindInvalidUTF8Codepoint(PChar(Msg), Length(Msg), False) > 0 then
    Msg := CeSysToUtf8(Msg);
  if (Msg <> '') and (Msg[length(Msg)] = LineEnding) then Delete(Msg, Length(Msg), 1);

  with Application do
  if (not Terminated) and (Application <> nil) and (AppInitialized in Flags) then
  begin
    DisableIdleHandler;
    try
      MsgResult := MessageDlg(
        Application.Title + ' - ' + rsMtError,
        rsMtError + ':' + LineEnding + Msg + LineEnding + LineEnding +
        Format(rsUnhandledExceptionMessage,
          [LineEnding + gErrorFile + LineEnding + LineEnding,
           StringReplace(rsMbIgnore, '&', '', [rfReplaceAll]),
           StringReplace(rsMbAbort, '&', '', [rfReplaceAll])]),
        mtError, [mbIgnore, mbAbort], 0, mbIgnore);

    finally
      EnableIdleHandler;
    end;
    if MsgResult = mrAbort then
    begin
      Flags := Flags + [AppNoExceptionMessages];
      Halt;
    end;
  end;
end;

procedure ShowException(e: Exception);
begin
  MessageDlg(Application.Title, rsMsgLogError + LineEnding + e.Message, mtError, [mbOK], 0);
end;

procedure HandleException(e: Exception; AThread: TThread);
begin
  HandleExceptionObj.HandleException(e, AThread);
end;

constructor THandleException.Create;
begin
  FHandleExceptionLock := TCriticalSection.Create;
end;

destructor THandleException.Destroy;
begin
  inherited;
  FreeAndNil(FHandleExceptionLock);
end;

procedure THandleException.HandleException(e: Exception; AThread: TThread);
var
  BackTrace: String;
begin
  if MainThreadID = GetCurrentThreadId then
  begin
    BackTrace := ExceptionToString;
    DCDebug(BackTrace);
    WriteExceptionToErrorFile(BackTrace);
    ShowExceptionDialog(e.Message);
  end
  else
  begin
    FHandleExceptionLock.Acquire;
    try
      FHandleExceptionMessage := e.Message;
      FHandleExceptionBackTrace := ExceptionToString;

      if FHandleExceptionBackTrace <> EmptyStr then
        DCDebug(FHandleExceptionBackTrace);

      TThread.Synchronize(AThread, @ShowException);

    finally
      FHandleExceptionLock.Release;
    end;
  end;
end;

procedure THandleException.ShowException;
begin
  WriteExceptionToErrorFile(FHandleExceptionBackTrace);
  ShowExceptionDialog(FHandleExceptionMessage);
end;

initialization
  HandleExceptionObj := THandleException.Create;

finalization
  FreeAndNil(HandleExceptionObj);

end.

