Skip to content

_runner

In-engine runner for agent-submitted source.

The agent host writes an entry script that calls :func:run with the AgentScriptContext it built. The runner executes the agent source in a fresh namespace, captures everything printed, serializes result to JSON, and reports all of it back through the context.

Invariant

This module must stay parseable by IronPython 2.7, IronPython 3.4 and CPython 3, because the host picks the engine per request.

Invariant

pyrevitlib is loaded live while the host is a compiled DLL that only changes after a rebuild and a Revit restart, so this module can meet an older AgentScriptContext. Read any context member added after the first release with getattr and a default that keeps the old behavior.

Attributes

SOURCE_NAME = '<agent-script>' module-attribute

Classes

RunTimedOut

Bases: BaseException

Raised inside the agent script once its time limit has passed.

Derives from BaseException so a script's except Exception can't swallow it. Python drops a trace function after its first raise on every engine, so a handler that caught it would end the deadline checks. The runner therefore narrows every bare except and except BaseException in the script to except Exception before compiling, and also records that the deadline passed so :func:run fails the run even if the script then ends normally.

Functions:

run(context)

Execute context.Source and report the outcome on context.

Parameters:

Name Type Description Default
context AgentScriptContext

run request and outcome sink from the host.

required
Note

Never raises. Script errors, including a failure to serialize result, are reported through context.SetError so the host can roll the run back.

Note

context.TimeoutSeconds is enforced with sys.settrace, so it stops Python code, including loops, but not a single blocking call such as a long Revit API call or time.sleep; the run fails with timeout once that call returns.

Warning

Raising from the trace function ends tracing, so the script's own handlers must not catch RunTimedOut. The source is compiled with every bare except and except BaseException narrowed to except Exception; a script can no longer catch KeyboardInterrupt or SystemExit that way. Suppression through an alias or contextlib.suppress(BaseException) still bypasses this, and on CPython the watchdog thread is the backstop.

Source code in pyrevitlib/pyrevit/agent/_runner.py
def run(context):
    """Execute ``context.Source`` and report the outcome on ``context``.

    Args:
        context (AgentScriptContext): run request and outcome sink from the host.

    Note:
        Never raises. Script errors, including a failure to serialize
        ``result``, are reported through ``context.SetError`` so the host can
        roll the run back.

    Note:
        ``context.TimeoutSeconds`` is enforced with ``sys.settrace``, so it
        stops Python code, including loops, but not a single blocking call
        such as a long Revit API call or ``time.sleep``; the run fails with
        ``timeout`` once that call returns.

    Warning:
        Raising from the trace function ends tracing, so the script's own
        handlers must not catch ``RunTimedOut``. The source is compiled with
        every bare ``except`` and ``except BaseException`` narrowed to
        ``except Exception``; a script can no longer catch ``KeyboardInterrupt``
        or ``SystemExit`` that way. Suppression through an alias or
        ``contextlib.suppress(BaseException)`` still bypasses this, and on
        CPython the watchdog thread is the backstop.
    """
    context.SetEngine(_implementation(), sys.version.split()[0], sys.version)
    source = context.Source
    linecache.cache[SOURCE_NAME] = (
        len(source),
        None,
        source.splitlines(True),
        SOURCE_NAME,
    )
    captured = StringIO()
    saved_stdout = sys.stdout
    saved_stderr = sys.stderr
    sys.stdout = captured
    sys.stderr = captured
    failed = False
    workspace = None
    namespace = None
    left_open = 0
    timeout_s = getattr(context, "TimeoutSeconds", None)
    state = {"timed_out": False}
    tracked_transactions = {}
    try:
        workspace = _enter_workspace(context.Workspace)
        namespace = _build_namespace(context)
        code, rewrite_problem = _compile_source(source)
        if rewrite_problem:
            print(
                "[pyRevit agent] Could not narrow the script's bare except "
                "handlers ({}); a handler that catches everything can swallow "
                "the timeout.".format(rewrite_problem)
            )
        started = time.time()
        stop_watchdog = None
        sys.settrace(_execution_tracer(timeout_s, state, tracked_transactions))
        if timeout_s:
            stop_watchdog = _start_watchdog(timeout_s, state)
        try:
            script_exit = None
            try:
                exec(code, namespace)
            except SystemExit as ex:
                script_exit = ex

            if script_exit is not None:
                if not _is_successful_exit(script_exit):
                    failed = True
                    context.SetError(
                        "SystemExit", _system_exit_message(script_exit), ""
                    )
            if not failed and "result" in namespace:
                try:
                    context.SetResult(
                        json.dumps(namespace["result"], default=_to_jsonable)
                    )
                except Exception as ex:
                    failed = True
                    context.SetError(
                        "ResultSerializationError",
                        _safe_text(ex),
                        _format_script_traceback(),
                    )

            if state["timed_out"] or (timeout_s and time.time() - started > timeout_s):
                raise RunTimedOut(timeout_s)
        finally:
            if stop_watchdog:
                stop_watchdog()
            sys.settrace(None)
    except RunTimedOut:
        failed = True
        context.SetError(
            "timeout",
            "The script ran longer than its {:g} second limit and was stopped; its "
            "changes were rolled back. Pass a larger timeout_s if the work needs "
            "more time.".format(timeout_s),
            _format_script_traceback(),
        )
    except Exception as ex:
        failed = True
        error_type, message = _describe_error(ex)
        context.SetError(error_type, message, _format_script_traceback())
    finally:
        if namespace is not None:
            left_open = _roll_back_open_transactions(namespace, tracked_transactions)
        sys.stdout = saved_stdout
        sys.stderr = saved_stderr
        _leave_workspace(context.Workspace, workspace)
        context.SetOutput(captured.getvalue())

    if left_open and not failed:
        context.SetError(
            "transaction_left_open",
            "The script left {} transaction(s) open; they were rolled back. Commit or "
            "roll back every transaction it starts.".format(left_open),
            None,
        )
        return
    if failed or "result" not in namespace:
        return