Page 1 of 1

append callstack to handled and unhandled exceptions

Posted: 26 Oct 2018, 14:20
by Sam_
quoth lexikos the wise
lexikos wrote:
28 Sep 2018, 19:41
You can obtain the call stack whenever you want by calling Exception in a loop as demonstrated elsewhere.

[...] If you throw the exception, you can get the call stack and attach it to the exception before throwing. If you catch an exception, the stack has already unwound to your handler, so getting it at that point might not be useful. For unhandled exceptions, you can register a callback with the new OnError function. The callback is called before the stack unwinds.

Improvements to exception handling are being planned for v2.
What I'd like is for every exception object to contain the callstack that lead to the error/exception being thrown. However, I have failed to come up with a robust solution I am happy with. The only method of retrieving the callstack that I have found is Coco's nifty Traceback() function (altered a bit to suit my needs). However, it seems it does not work on compiled scripts, so there's that :( ...
lexikos wrote:
02 Feb 2015, 04:28
Exception(x, some_negative_number) relies on the debugger's call stack, since AutoHotkey doesn't maintain a call stack as such just for script execution. The debugger code is omitted from AutoHotkeySC.bin, which becomes the executable part of each compiled script. Therefore, this function won't work in compiled scripts.

... with the possible exception of scripts compiled from AutoHotkey_H.exe, since they are based on a complete AutoHotkey executable.
For unhandled exceptions, I have come up with this:

Code: Select all

Global A_Quote:=Chr(34)

OnError("Traceback")
Main()
ExitApp

Main(){
		f()
}

a() {
	b()
}

b() {
	c()
}

c() {
	%cause% := error
}

f() {
	a()
}

Traceback(exception:="",actual:=0){
	i:=0, trace:="Traceback (most recent call first):`r`n", n:=actual?0:A_AhkVersion<"2"?1:2
	Loop {
		e:=Exception(".",offset:=-(A_Index+n))
		If (e.What=offset)
			Break
		trace.="  File " A_Quote e.File A_Quote ", line " e.Line ", in " e.What "`r`n"
	}
	;MsgBox % Trace
	If IsObject(exception)
		{
		exception.Extra.="`n`n" trace
		Return 0
		}
	Return trace
}
However, AHK's default error dialog seems to limit the string length allotted for the exception.Extra property, thus the full callstack (as can be seen by uncommenting MsgBox % Trace in the code above) is not shown:
Capture001.PNG
(15.91 KiB) Downloaded 24 times
One could of course write their own dialog (using MsgBox or Gui) to display the info from the exception object, but then they would have to come up with a way to retrieve the lines around the one that caused the error. I imagine this would be non-trivial with compiled scripts...

For handled exceptions, you could do something like the following:

Code: Select all

Global A_Quote:=Chr(34)

OnError("Traceback")
Main()
ExitApp

Main(){
	try {
		f()
		
	} catch e {
		ThrowMsg(16,"Error!","Exception thrown!`n`nWhat	=	" e.what "`nFile	=	" e.file "`nLine	=	" e.line "`nMessage	=	" e.message "`nExtra	=	" e.extra)
		}
}

a() {
	b()
}

b() {
	c()
}

c() {
	throw { what: (IsFunc(A_ThisFunc)?"function: " A_ThisFunc "()":"") A_Tab (IsLabel(A_ThisLabel)?"label: " A_ThisLabel:""), file: A_LineFile, line: A_LineNumber, message: "", extra: "`n`n" Traceback()}
}

f() {
	a()
}

Traceback(exception:="",actual:=0){
	i:=0, trace:="Traceback (most recent call first):`r`n", n:=actual?0:A_AhkVersion<"2"?1:2
	Loop {
		e:=Exception(".",offset:=-(A_Index+n))
		If (e.What=offset)
			Break
		trace.="  File " A_Quote e.File A_Quote ", line " e.Line ", in " e.What "`r`n"
	}
	; MsgBox % trace
	If IsObject(exception)
		{
		exception.Extra.="`n`n" trace
		Return 0
		}
	Return trace
}

;;;;; Core Background Functions ;;;;;

ThrowMsg(Options="",Title="",Text="",Timeout=""){
	If (Title="") AND (Text="") AND (Timeout=""){
		Gui +OwnDialogs
		MsgBox % Options
		}
	Else{
		Gui +OwnDialogs
		MsgBox, % Options , % Title , % Text , % Timeout
		}
}
However, there are some drawbacks I haven't figured out how to work around. As lexikos said, "If you catch an exception, the stack has already unwound to your handler" so I need to retrieve the callstack before reaching the catch block. Again, as lexikos said "If you throw the exception, you can get the call stack and attach it to the exception before throwing", which is what I have done in the example above. However, how do I achieve a similar result when the error is triggered by something like %cause% := error in the first example and not an exception object I generate and throw explicitly?

If anyone has alternate ways to approach this, or has any code improvements or suggestions, I'd love to hear them!

Sincerely,
Sam.

Re: append callstack to handled and unhandled exceptions

Posted: 28 Nov 2018, 14:06
by Sam_
I've been trying to improve my technique, and now use my own GUI to display the message to bypass the restrictions on the default MsgBox. Thanks to some clever code by garry, I can now also retrieve the line that threw the error regardless of whether or not the script is compiled (although you probably will not be able to if mpress was used when the script was compiled).

using try/catch:

Code: Select all

;
; AutoHotkey Version: 1.1.30.01
; Language:       English
; Platform:       Optimized for Windows 10
; Author:         Sam.
;

#NoEnv  ; Recommended for performance and compatibility with future AutoHotkey releases.
#Warn All, StdOut  ; Enable warnings to assist with detecting common errors.
SendMode Input  ; Recommended for new scripts due to its superior speed and reliability.
SetWorkingDir %A_ScriptDir%  ; Ensures a consistent starting directory.
#SingleInstance Force  ; Skips the dialog box and replaces the old instance automatically, which is similar in effect to the Reload command.


Global A_Quote:=Chr(34)


OnError("Traceback")
Main()
MsgBox Continue...
ExitApp

Main(){
	try {
		f()
		
	} catch e {
		;ThrowMsg(16,"Error!","Exception thrown!`n`nWhat	=	" e.what "`nFile	=	" e.file "`nLine	=	" e.line "`nMessage	=	" e.message "`nExtra	=	" e.extra)
		ExceptionErrorDlg(e)
		Return
		}
}

a() {
	b()
}

b() {
	c()
}

c() {
	;~ file:=FileOpen("filethatdoesnotexist.tmp","r")
	throw { what: (IsFunc(A_ThisFunc)?"function: " A_ThisFunc "()":"") A_Space (IsLabel(A_ThisLabel)?"label: " A_ThisLabel:""), file: A_LineFile, line: A_LineNumber, message: "We threw an exception.", extra: "Things went wrong...`n`n" Traceback()}
}

f() {
	a()
}

Traceback(exception:="",actual:=0){
	i:=0, trace:="", hdr:="Traceback (most recent call last):`n",  n:=actual?0:A_AhkVersion<"2"?1:2
	Loop {
		e:=Exception(".",offset:=-(A_Index+n))
		If (e.What=offset)
			Break
		trace:="  File " A_Quote e.File A_Quote ", line " e.Line " called " e.What "()`n" trace
	}
	;MsgBox % trace
	If IsObject(exception)
		{
		exception.Extra.="`n`n" hdr trace
		ExceptionErrorDlg(exception)
		Return 1
		}
	Return (hdr trace)
}

ExceptionErrorDlg(Content){
	Gui, ExcErrDlg:Color, White
	If !IsObject(Content)	; Content is plain text
		Gui, ExcErrDlg:Add, Text, , %Content%
	Else	; Content is an exception object
		{
		Content.Extra.="  File " A_Quote Content.File A_Quote ", line " Content.Line  (Content.What<>":="?" in " Content.What:"") ":`n"
		Contents:="Error: " Content.Message "`n`nSpecifically: " Content.Extra "`n`tLine#`n--->`t" GetScriptLine(Content.Line) "`n`nThe current thread will exit."
		Gui, ExcErrDlg:Add, Text, , %Contents%
		}
	Gui, ExcErrDlg:Add, Button,w100 gExcErrDlgGuiClose, OK
	Gui, ExcErrDlg:+ToolWindow +AlwaysOnTop
	Gui, ExcErrDlg:+HWNDhExcErrDlg
	Gui, ExcErrDlg:Show, , Error!
	WinWaitClose, % "ahk_id " hExcErrDlg
}

ExcErrDlgGuiClose:
	Gui,  ExcErrDlg:Destroy
Return


GetScriptLine(LineNum){
	If !A_IsCompiled
		FileReadLine, Line, %A_LineFile%, LineNum
	Else
		{
		SourceCode:=GetSourceCode()
		Loop, Parse, SourceCode, `n, `r
			{
			If (A_Index=LineNum+1)
				{
				Line:=A_LoopField
				Break
				}
			}
		}
	Return LineNum ": " Trim(Line," `t")
}

; EXE2AHK by garry
; https://autohotkey.com/boards/viewtopic.php?f=6&t=59015
GetSourceCode(){
	fileObj2:=scanFileForString(A_ScriptFullPath,"; <COMPILER","")
	If IsObject(fileObj2)
		{
		aah:=fileObj2.Read()
		fileObj2.Close()
		}
	Return aah
}

scanFileForString(filePath,searchString,stringEncoding:="UTF-8"){
	VarSetCapacity(pBin,StrPut(searchString,stringEncoding)*((stringEncoding="UTF-16"||stringEncoding="cp1200")?2:1),0)
	searchBinaryLength:=StrPut(searchString,&pBin,StrLen(searchString),stringEncoding)*((stringEncoding="UTF-16"||stringEncoding="cp1200")?2:1)
	Return scanFileForBinary(filePath,pBin,searchBinaryLength,stringEncoding)
}

scanFileForBinary(filePath,ByRef searchBinary,searchBinarylength,FileEncoding:="UTF-8"){
	If !FileExist(filePath)
		Return
	Offset:=0
	fileObj:=FileOpen(filePath,"r")
	Loop
	{
		If (fileObj.ReadUChar()=NumGet(searchBinary,Offset,"UChar"))
			{
			Offset++
			If (Offset=searchBinarylength)
				{
				fileObj.pos-=Offset
				Return fileObj
				}
			}
		Else If (offset)
			fileObj.pos-=(Offset-1), Offset:=0
	}Until fileObj.AtEOF
}
Using OnError:

Code: Select all

;
; AutoHotkey Version: 1.1.30.01
; Language:       English
; Platform:       Optimized for Windows 10
; Author:         Sam.
;

#NoEnv  ; Recommended for performance and compatibility with future AutoHotkey releases.
#Warn All, StdOut  ; Enable warnings to assist with detecting common errors.
SendMode Input  ; Recommended for new scripts due to its superior speed and reliability.
SetWorkingDir %A_ScriptDir%  ; Ensures a consistent starting directory.
#SingleInstance Force  ; Skips the dialog box and replaces the old instance automatically, which is similar in effect to the Reload command.


Global A_Quote:=Chr(34)

OnError("Traceback")
Main()
MsgBox Continue...
ExitApp

Main(){
	f()
}

a() {
	b()
}

b() {
	c()
}

c() {
	%cause% := error
}

f() {
	a()
}

Traceback(exception:="",actual:=0){
	i:=0, trace:="", hdr:="Traceback (most recent call last):`n",  n:=actual?0:A_AhkVersion<"2"?1:2
	Loop {
		e:=Exception(".",offset:=-(A_Index+n))
		If (e.What=offset)
			Break
		trace:="  File " A_Quote e.File A_Quote ", line " e.Line " called " e.What "()`n" trace
	}
	;MsgBox % trace
	If IsObject(exception)
		{
		exception.Extra.="`n`n" hdr trace
		ExceptionErrorDlg(exception)
		Return 1
		}
	Return (hdr trace)
}

ExceptionErrorDlg(Content){
	Gui, ExcErrDlg:Color, White
	If !IsObject(Content)	; Content is plain text
		Gui, ExcErrDlg:Add, Text, , %Content%
	Else	; Content is an exception object
		{
		Content.Extra.="  File " A_Quote Content.File A_Quote ", line " Content.Line  (Content.What<>":="?" in " Content.What:"") ":`n"
		Contents:="Error: " Content.Message "`n`nSpecifically: " Content.Extra "`n`tLine#`n--->`t" GetScriptLine(Content.Line) "`n`nThe current thread will exit."
		Gui, ExcErrDlg:Add, Text, , %Contents%
		}
	Gui, ExcErrDlg:Add, Button,w100 gExcErrDlgGuiClose, OK
	Gui, ExcErrDlg:+ToolWindow +AlwaysOnTop
	Gui, ExcErrDlg:+HWNDhExcErrDlg
	Gui, ExcErrDlg:Show, , Error!
	WinWaitClose, % "ahk_id " hExcErrDlg
}

ExcErrDlgGuiClose:
	Gui,  ExcErrDlg:Destroy
Return


GetScriptLine(LineNum){
	If !A_IsCompiled
		FileReadLine, Line, %A_LineFile%, LineNum
	Else
		{
		SourceCode:=GetSourceCode()
		Loop, Parse, SourceCode, `n, `r
			{
			If (A_Index=LineNum+1)
				{
				Line:=A_LoopField
				Break
				}
			}
		}
	Return LineNum ": " Trim(Line," `t")
}

; EXE2AHK by garry
; https://autohotkey.com/boards/viewtopic.php?f=6&t=59015
GetSourceCode(){
	fileObj2:=scanFileForString(A_ScriptFullPath,"; <COMPILER","")
	If IsObject(fileObj2)
		{
		aah:=fileObj2.Read()
		fileObj2.Close()
		}
	Return aah
}

scanFileForString(filePath,searchString,stringEncoding:="UTF-8"){
	VarSetCapacity(pBin,StrPut(searchString,stringEncoding)*((stringEncoding="UTF-16"||stringEncoding="cp1200")?2:1),0)
	searchBinaryLength:=StrPut(searchString,&pBin,StrLen(searchString),stringEncoding)*((stringEncoding="UTF-16"||stringEncoding="cp1200")?2:1)
	Return scanFileForBinary(filePath,pBin,searchBinaryLength,stringEncoding)
}

scanFileForBinary(filePath,ByRef searchBinary,searchBinarylength,FileEncoding:="UTF-8"){
	If !FileExist(filePath)
		Return
	Offset:=0
	fileObj:=FileOpen(filePath,"r")
	Loop
	{
		If (fileObj.ReadUChar()=NumGet(searchBinary,Offset,"UChar"))
			{
			Offset++
			If (Offset=searchBinarylength)
				{
				fileObj.pos-=Offset
				Return fileObj
				}
			}
		Else If (offset)
			fileObj.pos-=(Offset-1), Offset:=0
	}Until fileObj.AtEOF
}
Still can't run traceback on compiled code, and still can't run traceback after an AHK function or command in a try block has raised an exception caught in a catch block. Any suggestions for improvement are welcome.

Re: append callstack to handled and unhandled exceptions

Posted: 01 Feb 2019, 09:26
by Sam_
Using RHCP's classMemory, you can also load the source code from memory for a compiled script, even if it has been compressed with mpress. Adds a bit more utility.

Code: Select all

MsgBox % GetSourceCodeFromMemory()
ExitApp

GetSourceCodeFromMemory(){	; Works even if exe is compressed with mpress
	If A_IsCompiled
		{
		mem:=new _ClassMemory("ahk_pid " DllCall("GetCurrentProcessId")) ; create an object which can be used to read this script's memory.
		If IsObject(mem)
			{
			address:=mem.processPatternScan(,,60,67,79,77,80,73,76,69,82) ; <COMPILER
			If (address>0)
				Return mem.readString(address,0,"UTF-8")
			}
		}
	Return ""
}

; https://github.com/Kalamity/classMemory
; by RHCP
#include classMemory.ahk

Re: append callstack to handled and unhandled exceptions

Posted: 28 Mar 2019, 10:08
by Sam_
Latest version of my code can be found on GitHub: https://github.com/Sampsca/PS_ExceptionHandler

A copy of the version on GitHub as of 20190328 is provided below:

Code: Select all

;
; AutoHotkey Version: 1.1.30.01
; Language:       English
; Platform:       Optimized for Windows 10
; Author:         Sam.
;

;;;;;	Reference Documents	;;;;;
; https://autohotkey.com/docs/commands/Try.htm
; https://autohotkey.com/docs/commands/Throw.htm
; https://autohotkey.com/docs/commands/OnError.htm
; https://www.autohotkey.com/boards/viewtopic.php?f=76&t=58389
; https://www.autohotkey.com/boards/viewtopic.php?f=76&t=63130
; utility could be expanded using - https://github.com/Kalamity/classMemory
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;       PS_ExceptionHandler      ;;;;;
;;;;;    Copyright (c) 2019 Sam.     ;;;;;
;;;;;     Last Updated 20190328      ;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


;;; Include the following line in the auto-execute section of your script ;;;
; OnError("Traceback")

;;; When throwing an exception, it is best to format it as follows:
; throw Exception("We threw an exception",,"Things went wrong...`n`n" Traceback())

;;; Your try/catch blocks should look something like:
;~ try {
	;~ ; Do Stuff
;~ } catch e {
	;~ ExceptionErrorDlg(e)
;~ }


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Here is an example of usage ;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
/*
OnError("Traceback")
try {
	func()
} catch e {
	ExceptionErrorDlg(e)
}
ExitApp
func(){
	func2()
}
func2(){
	throw Exception("We threw an exception",,"Things went wrong...`n`n" Traceback())
}
#Include <PS_ExceptionHandler>
*/



;;;;; Core Functions ;;;;;

Traceback(exception:="",actual:=0){
	Local
	i:=0, trace:="", hdr:="Traceback (most recent call last):`n",  n:=actual?0:A_AhkVersion<"2"?1:2
	Loop {
		e:=Exception(".",offset:=-(A_Index+n))
		If (e.What=offset)
			Break
		trace:="  File " Chr(34) e.File Chr(34) ", line " e.Line " called " e.What "()`n" trace
	}
	;MsgBox % trace
	If IsObject(exception)
		{
		If !InStr(exception["Extra"],"Traceback (most recent call last):")
			exception.Extra.="`n`n" hdr trace
		ExceptionErrorDlg(exception)
		Return 1
		}
	Return (hdr trace)
}

ExceptionErrorDlg(Content){
	Local
	Gui, ExcErrDlg:Color, White
	If !IsObject(Content)	; Content is plain text
		Gui, ExcErrDlg:Add, Text, , %Content%
	Else	; Content is an exception object
		{
		Content.Extra.=(!InStr(Content["Extra"],(Str:="Traceback (most recent call last):"))?"`n`n" Str "`n":"") "  File " Chr(34) Content.File Chr(34) ", line " Content.Line  (Content.What<>":="?(Content.What<>""?" in " Content.What:""):"") ":`n"
		;~ Content.Extra.= "  File " Chr(34) Content.File Chr(34) ", line " Content.Line  (Content.What<>":="?" in " Content.What:"") ":`n"
		Contents:="Error: " Content.Message "`n`nSpecifically: " Content.Extra "`n`tLine#`n--->`t" GetScriptLine(Content.Line,Content.File) "`n`nThe current thread will exit."
		Global ExcErrDlgGuiText
		Gui, ExcErrDlg:Add, Text, vExcErrDlgGuiText, %Contents%
		}
	Gui, ExcErrDlg:Add, Button,w100 gExcErrDlgGuiClose, OK
	Gui, ExcErrDlg:Add, Button,w100 x+m yp gExcErrDlgGuiCopyClipboard, Copy to Clipboard
	Gui, ExcErrDlg:+ToolWindow +AlwaysOnTop
	Gui, ExcErrDlg:+HWNDhExcErrDlg
	Gui, ExcErrDlg:Show, , Error!
	WinWaitClose, % "ahk_id " hExcErrDlg
	Return % Contents
}
ExcErrDlgGuiCopyClipboard(){
	Global ExcErrDlgGuiText
	GuiControlGet, tmp, ExcErrDlg:, ExcErrDlgGuiText
	Clipboard:=StrReplace(StrReplace(tmp,"`r",""),"`n","`r`n")
}
ExcErrDlgGuiClose:
	Gui,  ExcErrDlg:Destroy
Return


GetScriptLine(LineNum,LineFile){
	Local
	Line:=""
	If !A_IsCompiled
		FileReadLine, Line, %LineFile%, %LineNum%
	Else
		{
		SourceCode:=GetSourceCode()
		Loop, Parse, SourceCode, `n, `r
			{
			If (A_Index=LineNum+1)
				{
				Line:=A_LoopField
				Break
				}
			}
		}
	Return LineNum ": " Trim(Line," `t")
}

; EXE2AHK by garry
; https://autohotkey.com/boards/viewtopic.php?f=6&t=59015
GetSourceCode(){
	fileObj2:=scanFileForString(A_ScriptFullPath,"; <COMPILER","")
	If IsObject(fileObj2)
		{
		aah:=fileObj2.Read()
		fileObj2.Close()
		}
	Return aah
}

scanFileForString(filePath,searchString,stringEncoding:="UTF-8"){
	VarSetCapacity(pBin,StrPut(searchString,stringEncoding)*((stringEncoding="UTF-16"||stringEncoding="cp1200")?2:1),0)
	searchBinaryLength:=StrPut(searchString,&pBin,StrLen(searchString),stringEncoding)*((stringEncoding="UTF-16"||stringEncoding="cp1200")?2:1)
	Return scanFileForBinary(filePath,pBin,searchBinaryLength,stringEncoding)
}

scanFileForBinary(filePath,ByRef searchBinary,searchBinarylength,FileEncoding:="UTF-8"){
	If !FileExist(filePath)
		Return
	Offset:=0
	fileObj:=FileOpen(filePath,"r")
	Loop
	{
		If (fileObj.ReadUChar()=NumGet(searchBinary,Offset,"UChar"))
			{
			Offset++
			If (Offset=searchBinarylength)
				{
				fileObj.pos-=Offset
				Return fileObj
				}
			}
		Else If (offset)
			fileObj.pos-=(Offset-1), Offset:=0
	}Until fileObj.AtEOF
}
There is a usage example commented out at the top, and it produces the following message:
ErrorDlg.PNG
(7.76 KiB) Downloaded 15 times
Any suggestions for improvement are welcome.