|
| 1 | +""" |
| 2 | +mathicsscript Interrupt routines. |
| 3 | +
|
| 4 | +Note: other environments may build on or use other interrupt handlers |
| 5 | +""" |
| 6 | + |
| 7 | +import signal |
| 8 | +import subprocess |
| 9 | +import sys |
| 10 | +from types import FrameType |
| 11 | +from typing import Callable, Optional |
| 12 | + |
| 13 | +from numpy import true_divide |
| 14 | + |
| 15 | +from mathics import settings |
| 16 | +from mathics.core.evaluation import Evaluation |
| 17 | +from mathics.core.interrupt import AbortInterrupt, ReturnInterrupt, TimeoutInterrupt |
| 18 | +from mathics.eval.stackframe import find_Mathics3_evaluation_method, get_eval_Expression |
| 19 | + |
| 20 | + |
| 21 | +# See also __main__'s interactive_eval_loop |
| 22 | +def inspect_eval_loop(evaluation: Evaluation): |
| 23 | + """ |
| 24 | + A read eval/loop for an Interrupt's "inspect" command. |
| 25 | + """ |
| 26 | + shell = evaluation.shell |
| 27 | + if shell is not None: |
| 28 | + was_inside_interrupt = shell.is_inside_interrupt |
| 29 | + shell.is_inside_interrupt = True |
| 30 | + else: |
| 31 | + was_inside_interrupt = False |
| 32 | + |
| 33 | + previous_recursion_depth = evaluation.recursion_depth |
| 34 | + while True: |
| 35 | + try: |
| 36 | + # Reset line number within an In[] line number. |
| 37 | + # Note: this is not setting as, say, In[5] |
| 38 | + # to back to In[1], but instead it sets the line number position *within* |
| 39 | + # In[5]. The user input for "In[5]" might have several continuation lines. |
| 40 | + if shell is not None and hasattr(shell, "lineno"): |
| 41 | + shell.lineno = 0 |
| 42 | + |
| 43 | + query, source_code = evaluation.parse_feeder_returning_code(shell) |
| 44 | + # show_echo(source_code, evaluation) |
| 45 | + if len(source_code) and source_code[0] == "!" and shell is not None: |
| 46 | + subprocess.run(source_code[1:], shell=True) |
| 47 | + if shell.definitions is not None: |
| 48 | + shell.definitions.increment_line_no(1) |
| 49 | + continue |
| 50 | + if query is None: |
| 51 | + continue |
| 52 | + result = evaluation.evaluate(query, timeout=settings.TIMEOUT) |
| 53 | + if result is not None and shell is not None: |
| 54 | + shell.print_result(result, prompt=False, strict_wl_output=True) |
| 55 | + except TimeoutInterrupt: |
| 56 | + print("\nTimeout occurred - ignored.") |
| 57 | + pass |
| 58 | + except ReturnInterrupt: |
| 59 | + evaluation.last_eval = None |
| 60 | + evaluation.exc_result = None |
| 61 | + evaluation.message("Interrupt", "dgend") |
| 62 | + raise |
| 63 | + except KeyboardInterrupt: |
| 64 | + print("\nKeyboardInterrupt") |
| 65 | + except EOFError: |
| 66 | + print() |
| 67 | + raise |
| 68 | + except SystemExit: |
| 69 | + # raise to pass the error code on, e.g. Quit[1] |
| 70 | + raise |
| 71 | + finally: |
| 72 | + evaluation.recursion_depth = previous_recursion_depth |
| 73 | + if shell is not None: |
| 74 | + shell.is_inside_interrupt = was_inside_interrupt |
| 75 | + |
| 76 | + |
| 77 | +def Mathics3_interrupt_handler( |
| 78 | + evaluation: Optional[Evaluation], interrupted_frame: FrameType, print_fn: Callable |
| 79 | +): |
| 80 | + |
| 81 | + shell = evaluation.shell |
| 82 | + incolors = shell.incolors |
| 83 | + if hasattr(shell, "bottom_toolbar"): |
| 84 | + from mathicsscript.completion import InterruptCompleter |
| 85 | + |
| 86 | + is_prompt_toolkit = True |
| 87 | + use_HTML = True |
| 88 | + completer = InterruptCompleter() |
| 89 | + else: |
| 90 | + is_prompt_toolkit = False |
| 91 | + use_HTML = False |
| 92 | + completer = None |
| 93 | + while True: |
| 94 | + try: |
| 95 | + prompt = ( |
| 96 | + "<b>interrupt> </b>" |
| 97 | + if is_prompt_toolkit |
| 98 | + else f"{incolors[0]}interrupt> {incolors[3]}" |
| 99 | + ) |
| 100 | + user_input = shell.read_line(prompt, completer, use_HTML).strip() |
| 101 | + if user_input in ("a", "abort"): |
| 102 | + print_fn("aborting") |
| 103 | + raise AbortInterrupt |
| 104 | + elif user_input in ("continue", "c"): |
| 105 | + print_fn("continuing") |
| 106 | + break |
| 107 | + elif user_input in ("exit", "quit"): |
| 108 | + print_fn("Mathics3 exited because of an interrupt.") |
| 109 | + sys.exit(3) |
| 110 | + elif user_input in ("inspect", "i"): |
| 111 | + print_fn("inspecting") |
| 112 | + if evaluation is not None: |
| 113 | + evaluation.message("Interrupt", "dgbgn") |
| 114 | + inspect_eval_loop(evaluation) |
| 115 | + |
| 116 | + elif user_input in ("show", "s"): |
| 117 | + # In some cases we can better, by going back to the caller |
| 118 | + # and reconstructing the actual call with arguments. |
| 119 | + eval_frame = find_Mathics3_evaluation_method(interrupted_frame) |
| 120 | + if eval_frame is None: |
| 121 | + continue |
| 122 | + eval_method_name = eval_frame.f_code.co_name |
| 123 | + eval_method = getattr(eval_frame.f_locals.get("self"), eval_method_name) |
| 124 | + if eval_method: |
| 125 | + print_fn(eval_method.__doc__) |
| 126 | + eval_expression = get_eval_Expression() |
| 127 | + if eval_expression is not None: |
| 128 | + print_fn(shell.fmt_fn(eval_expression)) |
| 129 | + break |
| 130 | + elif user_input in ("trace", "t"): |
| 131 | + print_fn("tracing") |
| 132 | + else: |
| 133 | + print_fn( |
| 134 | + """Your options are: |
| 135 | + abort (or a) to abort current calculation |
| 136 | + continue (or c) to continue |
| 137 | + exit (or quit) to exit Mathics3 |
| 138 | + inspect (or i) to enter an interactive dialog |
| 139 | + show (or s) to show current operation (and then continue) |
| 140 | +""" |
| 141 | + ) |
| 142 | + except KeyboardInterrupt: |
| 143 | + print_fn("\nKeyboardInterrupt") |
| 144 | + except EOFError: |
| 145 | + print_fn("") |
| 146 | + break |
| 147 | + except TimeoutInterrupt: |
| 148 | + # There might have been a Pause[] set before we entered |
| 149 | + # this handler. If that happens, we can clear the |
| 150 | + # error. Ideally the interrupt REPL would would have clear |
| 151 | + # all timeout signals, but Python doesn't support that, as |
| 152 | + # far as I know. |
| 153 | + # |
| 154 | + # Here, we note we have time'd out. This also silences |
| 155 | + # other handlers that we've handled this. |
| 156 | + if evaluation is not None: |
| 157 | + evaluation.timeout = True |
| 158 | + break |
| 159 | + except ReturnInterrupt: |
| 160 | + # the interrupt shell probably isssued a Return[]. |
| 161 | + # Respect that. |
| 162 | + break |
| 163 | + except RuntimeError: |
| 164 | + break |
| 165 | + finally: |
| 166 | + pass |
| 167 | + |
| 168 | + |
| 169 | +def Mathics3_basic_signal_handler(sig: int, interrupted_frame: Optional[FrameType]): |
| 170 | + """ |
| 171 | + Custom signal handler for SIGINT (Ctrl+C). |
| 172 | + """ |
| 173 | + evaluation: Optional[Evaluation] = None |
| 174 | + # Find an evaluation object to pass to the Mathics3 interrupt handler |
| 175 | + while interrupted_frame is not None: |
| 176 | + if ( |
| 177 | + evaluation := interrupted_frame.f_locals.get("evaluation") |
| 178 | + ) is not None and isinstance(evaluation, Evaluation): |
| 179 | + break |
| 180 | + interrupted_frame = interrupted_frame.f_back |
| 181 | + print_fn = evaluation.print_out if evaluation is not None else print |
| 182 | + print_fn("") |
| 183 | + if interrupted_frame is None: |
| 184 | + print("Unable to find Evaluation frame to start on") |
| 185 | + Mathics3_interrupt_handler(evaluation, interrupted_frame, print_fn) |
| 186 | + |
| 187 | + |
| 188 | +def Mathics3_USR1_signal_handler(sig: int, interrupted_frame: Optional[FrameType]): |
| 189 | + """ |
| 190 | + Custom signal handler for SIGUSR1. When we get this signal, try to |
| 191 | + find an Expression that is getting evaluated, and print that. Then |
| 192 | + continue. |
| 193 | + """ |
| 194 | + shell = None |
| 195 | + print_fn = print |
| 196 | + while interrupted_frame is not None: |
| 197 | + if ( |
| 198 | + evaluation := interrupted_frame.f_locals.get("evaluation") |
| 199 | + ) is not None and isinstance(evaluation, Evaluation): |
| 200 | + print_fn = evaluation.shell.errmsg |
| 201 | + break |
| 202 | + interrupted_frame = interrupted_frame.f_back |
| 203 | + |
| 204 | + print_fn(f"USR1 ({sig}) interrupt") |
| 205 | + if (eval_expression := get_eval_Expression()) is not None: |
| 206 | + # If eval string is long, take just the first 100 characters |
| 207 | + # of it. |
| 208 | + if shell is not None: |
| 209 | + eval_expression_str = shell.fmt_fn(eval_expression) |
| 210 | + else: |
| 211 | + eval_expression_str = str(eval_expression) |
| 212 | + |
| 213 | + if len(eval_expression_str) > 100: |
| 214 | + eval_expression_str = eval_expression_str[:100] + "..." |
| 215 | + |
| 216 | + print(f"Expression: {eval_expression_str}") |
| 217 | + |
| 218 | + |
| 219 | +def setup_signal_handler(): |
| 220 | + signal.signal(signal.SIGINT, Mathics3_basic_signal_handler) |
| 221 | + signal.signal(signal.SIGUSR1, Mathics3_USR1_signal_handler) |
0 commit comments