-
Notifications
You must be signed in to change notification settings - Fork 567
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Only show stack trace in debug mode #1860
Conversation
Hi @williballenthin thank you for these comments! I certainly see your point about avoiding global variables, and have rewritten the code to avoid them and to make the decision logic in the exception handler depend on argparse. I have pushed a new draft of the code, but I have some questions about it:
EDIT: I have learned that
However, using a class may help to store and set the exception_information values. I'll draft a prototype and see if this can avoid using global variables here. |
Hi @williballenthin what do you think of this class-based replacement for
The |
Hi @williballenthin I hope your week is going well. Are there things you think I should still improve about this code? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
concepts are good, suggestions inline.
i'm going to be mostly offline for a few weeks. @mr-tz would you please help with final reviews?
capa/main.py
Outdated
@@ -1133,6 +1154,8 @@ def main(argv: Optional[List[str]] = None): | |||
if ret is not None and ret != 0: | |||
return ret | |||
|
|||
sys.excepthook = last_resort_exception_handler(args, *exception_information) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where does exception_information
come from? i don't see it in scope (but im on mobile so maybe i missed it).
capa/main.py
Outdated
value (str): exception type | ||
traceback (TracebackType): the Type of a traceback object | ||
""" | ||
if "-d" or "--debug" in args: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use argparse parsed data, like if args.debug: ...
capa/main.py
Outdated
@@ -1087,6 +1088,26 @@ def handle_common_args(args): | |||
args.signatures = sigs_path | |||
|
|||
|
|||
def last_resort_exception_handler(args, exctype, value, traceback) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
an alternative design is to check the args in main
and install one of two different functions, so you don't have to parse args
into the handler
Hi @williballenthin thank you for this feedback, your suggestions are very helpful! I apologize - I did not ask my question clearly above. Would you or @mr-tz be able to review this class-based implementation (also described in a comment above)? If so, please let me know if there are any issues with this implementation, or if you have any further suggestions :)
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also see this section https://www.bitecode.dev/i/125718336/you-are-not-the-only-one-doing-this for an alternative, rich may be overkill, but pretty-traceback could be nice. Both can also give other insight on how to do this.
capa/main.py
Outdated
if "-d" or "--debug" in args: | ||
sys.__excepthook__(exctype, value, traceback) | ||
else: | ||
print(f"Unexpected exception raised: {exctype}. Please run capa in Python development mode " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
print(f"Unexpected exception raised: {exctype}. Please run capa in Python development mode " | |
print(f"Unexpected exception raised: {exctype}. Please run capa in debug mode (-d/--debug)" |
capa/main.py
Outdated
sys.__excepthook__(exctype, value, traceback) | ||
else: | ||
print(f"Unexpected exception raised: {exctype}. Please run capa in Python development mode " | ||
"to see the stack trace (https://docs.python.org/3/library/devmode.html#devmode). " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"to see the stack trace (https://docs.python.org/3/library/devmode.html#devmode). " | |
"to see the stack trace" |
ah i see i'm sorry @aaronatp for misunderstanding i see now from your class-based design that you're trying to access both CLI args and the exception information from a function that only receives the exception information. using a class for this is reasonable, but as you've found, needs a good bit of code (20-30 lines of variables shuffled around). there are two design patterns i'd recommend, both derived from closures. a closure is a function that has access to data that was in scope when the function was created. it's a way to bind some data to logic that will be invoked later. def make_hook(args):
def hook(ex1, ex2, ex3):
if args.debug:
print(ex1)
else:
print("there was an error, use debug mode")
return hook
sys.excepthook = make_hook(args) you could also use functools.partial for this: https://docs.python.org/3/library/functools.html#functools.partial def handler(args, ex1, ex2, ex3):
...
sys.excepthook = functools.partial(handler, args) sorry i'm on mobile so im a little terse and don't have good references. if the above isn't immediately obvious, take a look at some of the blog posts/tutorials on using closures from Python. i'm also happy to explain more (as i have time). |
Hi @mr-tz and @williballenthin, thank you both for your feedback! I have implemented the closure that @williballenthin suggested and have included that below. I also included a try-except statement for worst-case-scenario error handling. I have also edited the error message in line with your suggestions. Please let me know if you have any further questions or suggestions :) I also had a look at the pretty_traceback library and it looks interesting! I noticed, however, that the codebase is not frequently updated and it has few maintainers. (The last commit was September 7th.) Do you think that this could potentially lead to more headaches down the line? For example, if newer versions of python are released that are not backwards compatible with pretty_traceback. But if you don't think this is a significant issue, please let me know - I would be happy to implement it in capa!
|
Awesome, can you please update the PR so we can discuss/review inline and see the CI status? Thanks for researching the maintenance status and other details on the library. If it's not much code we should just keep it in capa so we have full control and can update it easily. I feel we do not need prettier stack traces. |
Replacement for the default sys.excepthook function. Prints traceback in debug mode. Otherwise, prints helpful message.
Hi @mr-tz thanks for your comments! I have updated the code and will now work on making sure it passes the tests. Please let me know if you have any thoughts :) Also no worries about the pretty_traceback, thank you for letting me know! EDIT: Also, one thing to note is that |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is coming together nicely, just a few remaining comments before we can merge (I think)
capa/main.py
Outdated
args (argparse.Namespace): parsed command-line arguments | ||
exctype (Exception): exception class | ||
value (str): exception instance | ||
traceback (TracebackType): a traceback object |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
alternatively/additionally set the types in the function so mypy can do type checking
capa/main.py
Outdated
def handle_exception_information(args): | ||
try: | ||
nonlocal exctype, value, traceback | ||
if "-d" or "--debug" in args: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is args parsed at this point, so can we do args.debug
?
capa/main.py
Outdated
|
||
except Exception as e: # pylint: disable=broad-except | ||
logger.error("%s", str(e)) | ||
return # should this return an object like some other capa try-except statements, e.g., E_CORRUPT_FILE or E_INVALID_FILE_TYPE? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, if this returns the value that is then used as program return code
since it's not known an error name that indicates this would be good, e.g. E_UNHANDLED
?
Hi @mr-tz, for the custom exception handler function test, I have been thinking about: 1) checking whether a traceback is printed if the user uses the debugging flag; and, 2) whether the traceback was produced by the custom handler (the default output of sys.excepthook is printing a traceback so we need to distinguish sys.excepthook's traceback from the custom handler in order to see if the custom handler is working correctly). The first goal seems quite achievable. The second goal is giving me more trouble. Assuming a traceback is printed, we want to see whether the custom handler called the "sys.excepthook" traceback-printing function, or whether the default "sys.excepthook" was the one that called it. I thought that we could distinguish the custom handler traceback from the default traceback by decorating the custom handler with a function that prints some extra words, such as "The custom handler was called." This would enable us to confirm that a traceback was produced by the custom handler. However, we replace sys.excepthook with a function whose variables are unbounded. In other words, the replacement function's variables are not defined or initialized. This fact has made it somewhat difficult to implement a decorator for the custom handler because decorators usually modify a function's behavior, but when we decorate an unbounded function's variables, we make no reference to its variables. From my research, one solution may be to use metaclasses, but that seems like disproportionately heavy machinery. There may be other ways of distinguishing the custom handler's traceback from the default sys.excepthook's, but I have not found any yet, and they may be much more cobbled together. Can you help me understand how I can create a decorator that's applied to a function with unbounded variables that also returns an unbounded function? This would help me distinguish the custom handler's traceback from the default sys.excepthook's, and therefore conclude that the custom handler is functioning correctly. |
Can we look for certain output parts that we always expect for the respective handler? Let's keep it as simple as possible. |
Hi @mr-tz, I looked at some examples of sys.excepthook in other projects and have restructured the function here based on what I saw. I think it's now simpler and better. Now, the custom handler only prints a simple informative message - however, the custom handler is only used if the program is not run in debug mode. In debug mode, the default sys.excepthook is used, which prints a traceback. This is similar to Meta's Buck repo (https://github.com/facebook/buck/blob/9c7c421e49f4d92d67321f18c6d1cd90974c77c4/python-dsl/buck_parser/buck.py#L2299). When we simplify the custom handler, we may not need to test it. Since sys.excepthook (respectively, the custom handler) is only invoked when there is an unhandled exception, and essentially terminates a program, I have had trouble figuring out how to: 1) run the custom handler in a test; and then, 2) continue the rest of the tests. When we make custom handler essentially a print statement, we may not need to design tests for it. Indeed, repos that use custom sys.excepthook handlers often implement simple handlers, but I have not seen any repos test the handlers specifically. What do you think of this? Do you see any simple way we could test the custom handler but continue executing the program afterwards? EDIT: Also, there is a type annotation issue with the exctype parameter of the custom exception handler. The default sys.excepthook function expects exctype to have a "type[BaseException]" type annotation. However, since the built-in type() function was not subscriptable in Python 3.8, using that type annotation would not build on Python 3.8 platforms. Something like "Union[type[BaseException], typing.Type[BaseException]]" would fail to build for the same reason. Although it would still cause an issue for the mypy test, one option would be to not give exctype any type annotation in the custom handler's function definition. However, even though it's unlikely, I am wondering whether this could be a security issue if someone manages to write any data type to the exctype parameter of the sys.excepthook function. What do you suggest? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
great, this is much simpler and I don't think we need tests here, please address my comments (and the CI fails) and then we can merge
capa/main.py
Outdated
@@ -112,6 +113,7 @@ | |||
E_MISSING_CAPE_STATIC_ANALYSIS = 21 | |||
E_MISSING_CAPE_DYNAMIC_ANALYSIS = 22 | |||
E_EMPTY_REPORT = 23 | |||
E_UNHANDLED = 24 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
E_UNHANDLED = 24 |
capa/main.py
Outdated
traceback: TracebackType, | ||
): | ||
""" | ||
if not in debug mode, prints helpful message on unhandled exceptions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if not in debug mode, prints helpful message on unhandled exceptions | |
prints friendly message on unhandled exceptions to regular users, debug mode shows regular stack trace |
capa/main.py
Outdated
args: | ||
# TODO(aaronatp): Once capa drops support for Python 3.8, move this to the type annotation in the function parameters | ||
exctype (type[BaseException]): exception class |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this still accurate or can it be removed?
Hi @mr-tz, thank you for your comments! I have just fixed the first 2 of 3. For the third one, I am working to resolve a type annotation issue with the exctype parameter of the custom exception handler. The default sys.excepthook function expects exctype to have a "type[BaseException]" type annotation. However, since the built-in type() function was not subscriptable in Python 3.8, using that type annotation would not build on Python 3.8 platforms. Something like "Union[type[BaseException], typing.Type[BaseException]]" would fail to build for the same reason. One option would be to not give exctype any type annotation in the custom handler's function definition (although this would cause the mypy test to fail). Even though it's unlikely, I am wondering whether this could be a security issue if someone manages to write any data type to the exctype parameter of the sys.excepthook function. What do you suggest? |
Hi @mr-tz, after reading a bit further, I see it is possible to disable specific mypy warnings. I have just done that - please let me know if you would rather I not disable it! |
Ignoring here is fine. I think we also need an issue and link for the TODO in the code. |
@mr-tz great thank you, just done that! Please let me know if you have any more comments! |
capa/main.py
Outdated
print( | ||
f"Unexpected exception raised: {exctype}. Please run capa in debug mode (-d/--debug) " | ||
+ "to see the stack trace. Please also report your issue on the capa GitHub page so we " | ||
+ "can improve the code! (https://github.com/mandiant/capa/issues)" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
print( | |
f"Unexpected exception raised: {exctype}. Please run capa in debug mode (-d/--debug) " | |
+ "to see the stack trace. Please also report your issue on the capa GitHub page so we " | |
+ "can improve the code! (https://github.com/mandiant/capa/issues)" | |
) | |
if exctype is KeyboardInterrupt: | |
print("KeyboardInterrupt detected, program terminated") | |
else: | |
print( | |
f"Unexpected exception raised: {exctype}. Please run capa in debug mode (-d/--debug) " | |
+ "to see the stack trace. Please also report your issue on the capa GitHub page so we " | |
+ "can improve the code: https://github.com/mandiant/capa/issues" | |
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what do you think of this to also handle CTRL-C (and not say unhandled exception there)
@mr-tz this change is good, thank you! |
Great, thank you! |
thank you @aaronatp ! |
Checklist
This PR creates a new function
last_resort_exception_handler
to be a last line of defense for exceptions that are not otherwise handled. It will show the stack trace in dev mode (if ran with the-d
or--debug
flags).EDIT: I'll work on making this code pass the tests!