diff --git a/mathics/builtin/datentime.py b/mathics/builtin/datentime.py index 65dcf058d..df6c8d9ed 100644 --- a/mathics/builtin/datentime.py +++ b/mathics/builtin/datentime.py @@ -27,11 +27,7 @@ from mathics.core.convert.expression import to_expression, to_mathics_list from mathics.core.convert.python import from_python from mathics.core.element import ImmutableValueMixin -from mathics.core.evaluation import ( - Evaluation, - TimeoutInterrupt, - run_with_timeout_and_stack, -) +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.symbols import Symbol @@ -1070,6 +1066,7 @@ def evaluate(self, evaluation): if sys.platform != "emscripten": + import stopit class TimeConstrained(Builtin): """ @@ -1111,18 +1108,22 @@ def eval_3(self, expr, t, failexpr, evaluation): evaluation.message("TimeConstrained", "timc", t) return try: - t = float(t.to_python()) - evaluation.timeout_queue.append((t, datetime.now().timestamp())) + timeout = float(t.to_python()) + evaluation.timeout_queue.append((timeout, datetime.now().timestamp())) request = lambda: expr.evaluate(evaluation) - res = run_with_timeout_and_stack(request, t, evaluation) - except TimeoutInterrupt: - evaluation.timeout_queue.pop() - return failexpr.evaluate(evaluation) + done = False + with stopit.ThreadingTimeout(timeout) as to_ctx_mgr: + assert to_ctx_mgr.state == to_ctx_mgr.EXECUTING + result = request() + done = True + if done: + evaluation.timeout_queue.pop() + return result except Exception: evaluation.timeout_queue.pop() raise evaluation.timeout_queue.pop() - return res + return failexpr.evaluate(evaluation) class TimeZone(Predefined): diff --git a/mathics/core/evaluation.py b/mathics/core/evaluation.py index f619705a2..7abc51cdf 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -4,8 +4,6 @@ import sys import time from abc import ABC -from queue import Queue -from threading import Thread, stack_size as set_thread_stack_size from typing import Any, Callable, Dict, List, Optional, Tuple, Union, overload from mathics_scanner import TranslateError @@ -57,16 +55,6 @@ SymbolPrePrint = Symbol("System`$PrePrint") SymbolPost = Symbol("System`$Post") - -def _thread_target(request, queue) -> None: - try: - result = request() - queue.put((True, result)) - except BaseException: - exc_info = sys.exc_info() - queue.put((False, exc_info)) - - # MAX_RECURSION_DEPTH gives the maximum value allowed for $RecursionLimit. it's usually set to its # default settings.DEFAULT_MAX_RECURSION_DEPTH. @@ -96,54 +84,6 @@ def set_python_recursion_limit(n) -> None: raise OverflowError -def run_with_timeout_and_stack(request, timeout, evaluation): - """ - interrupts evaluation after a given time period. Provides a suitable stack environment. - """ - - # only use set_thread_stack_size if max recursion depth was changed via the environment variable - # MATHICS_MAX_RECURSION_DEPTH. if it is set, we always use a thread, even if timeout is None, in - # order to be able to set the thread stack size. - - if MAX_RECURSION_DEPTH > settings.DEFAULT_MAX_RECURSION_DEPTH: - set_thread_stack_size(python_stack_size(MAX_RECURSION_DEPTH)) - elif timeout is None: - return request() - - queue = Queue(maxsize=1) # stores the result or exception - thread = Thread(target=_thread_target, args=(request, queue)) - thread.start() - - # Thead join(timeout) can leave zombie threads (we are the parent) - # when a time out occurs, but the thread hasn't terminated. See - # https://docs.python.org/3/library/multiprocessing.shared_memory.html - # for a detailed discussion of this. - # - # To reduce this problem, we make use of specific properties of - # the Mathics3 evaluator: if we set "evaluation.timeout", the - # next call to "Expression.evaluate" in the thread will finish it - # immediately. - # - # However this still will not terminate long-running processes - # in Sympy or or libraries called by Mathics3 that might hang or run - # for a long time. - thread.join(timeout) - if thread.is_alive(): - evaluation.timeout = True - while thread.is_alive(): - time.sleep(0.001) - pass - evaluation.timeout = False - evaluation.stopped = False - raise TimeoutInterrupt() - - success, result = queue.get() - if success: - return result - else: - raise result[1].with_traceback(result[2]) - - class _Out(KeyComparable): def __init__(self) -> None: self.is_message = False @@ -296,7 +236,7 @@ def evaluate(): try: try: - result = run_with_timeout_and_stack(evaluate, timeout, self) + result = evaluate() except KeyboardInterrupt: if self.catch_interrupt: self.exc_result = SymbolAborted diff --git a/mathics/eval/sympy.py b/mathics/eval/sympy.py index 2602dec97..4547ee785 100644 --- a/mathics/eval/sympy.py +++ b/mathics/eval/sympy.py @@ -2,9 +2,6 @@ Evaluation of SymPy functions """ -import sys -from queue import Queue -from threading import Thread from typing import Optional import sympy @@ -15,9 +12,7 @@ from mathics.core.evaluation import Evaluation -def eval_sympy_unconstrained( - self, z: BaseElement, evaluation: Evaluation -) -> Optional[BaseElement]: +def eval_sympy(self, z: BaseElement, evaluation: Evaluation) -> Optional[BaseElement]: """ Evaluate element `z` converting it to SymPy and back to Mathics3. If an exception is raised we return None. @@ -33,52 +28,3 @@ def eval_sympy_unconstrained( return from_sympy(tracing.run_sympy(sympy_fn, *sympy_args)) except Exception: return - - -def eval_sympy_with_timeout( - self, z: BaseElement, evaluation: Evaluation -) -> Optional[BaseElement]: - """ - Evaluate an element `z` converting it to SymPy, - and back to Mathics3. - If an exception is raised we return None. - - This version is run in a thread, and checked for evaluation timeout. - """ - - if evaluation.timeout is None: - return eval_sympy_unconstrained(self, z, evaluation) - - def _thread_target(queue) -> None: - try: - result = eval_sympy_unconstrained(self, z, evaluation) - queue.put((True, result)) - except BaseException: - exc_info = sys.exc_info() - queue.put((False, exc_info)) - - queue = Queue(maxsize=1) # stores the result or exception - - thread = Thread(target=_thread_target, args=(queue,)) - thread.start() - while thread.is_alive(): - thread.join(0.001) - if evaluation.timeout: - # I can kill the thread. - # just leave it... - return None - - # pick the result and return - success, result = queue.get() - if success: - return result - else: - raise result[1].with_traceback(result[2]) - - -# Common top-level evaluation SymPy "eval" function: -eval_sympy = ( - eval_sympy_unconstrained - if sys.platform in ("emscripten",) - else eval_sympy_with_timeout -) diff --git a/pyproject.toml b/pyproject.toml index 1f0b28461..083872b99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ # Pillow 9.1.0 supports BigTIFF with big-endian byte order. # ExampleData image hedy.tif is in this format. # Pillow 9.2 handles sunflowers.jpg + "stopit; platform_system != 'Emscripten'", "pillow >= 9.2", "pint", "python-dateutil",