|
3 | 3 | import asyncio
|
4 | 4 | import functools
|
5 | 5 | import logging
|
| 6 | +from collections.abc import Generator |
6 | 7 | from functools import cache
|
7 |
| -from threading import Event |
| 8 | +from threading import Event, Lock |
| 9 | +from typing import Any, cast |
8 | 10 |
|
9 | 11 | import bluesky.preprocessors as bpp
|
10 |
| -from bluesky.run_engine import RunEngine |
11 |
| -from bluesky.utils import DuringTask |
| 12 | +from bluesky.run_engine import RunEngine, RunEngineResult |
| 13 | +from bluesky.utils import DuringTask, Msg, RunEngineControlException, RunEngineInterrupted |
12 | 14 |
|
13 | 15 | from ibex_bluesky_core.callbacks import DocLoggingCallback
|
14 | 16 | from ibex_bluesky_core.preprocessors import add_rb_number_processor
|
15 | 17 |
|
16 |
| -__all__ = ["get_run_engine"] |
| 18 | +__all__ = ["get_run_engine", "run_plan"] |
17 | 19 |
|
18 | 20 |
|
19 | 21 | from ibex_bluesky_core.plan_stubs import CALL_QT_AWARE_MSG_KEY, CALL_SYNC_MSG_KEY
|
@@ -106,3 +108,81 @@ def get_run_engine() -> RunEngine:
|
106 | 108 | RE.preprocessors.append(functools.partial(bpp.plan_mutator, msg_proc=add_rb_number_processor))
|
107 | 109 |
|
108 | 110 | return RE
|
| 111 | + |
| 112 | + |
| 113 | +_RUN_PLAN_LOCK = Lock() # Explicitly *not* an RLock - RunEngine is not reentrant. |
| 114 | + |
| 115 | + |
| 116 | +def run_plan( |
| 117 | + plan: Generator[Msg, Any, Any], |
| 118 | + **metadata_kw: Any, # noqa ANN401 - this really does accept anything serializable |
| 119 | +) -> RunEngineResult: |
| 120 | + """Run a plan. |
| 121 | +
|
| 122 | + .. Warning:: |
| 123 | +
|
| 124 | + The usual way to run a plan in bluesky is by calling ``RE(plan(...))`` interactively. |
| 125 | + An ``RE`` object is already available in recent versions of the IBEX user interface, |
| 126 | + or can be acquired by calling ``get_run_engine()``. |
| 127 | +
|
| 128 | + Use of this function is **not recommended**, but it is nevertheless provided as an escape |
| 129 | + hatch for workflows which would otherwise be difficult to express or where parts of scanning |
| 130 | + scripts have not, or cannot, be migrated to bluesky. |
| 131 | +
|
| 132 | + Args: |
| 133 | + plan (positional-only): The plan to run. This is typically a generator instance. |
| 134 | + metadata_kw (optional, keyword-only): Keyword arguments (metadata) to pass to the bluesky |
| 135 | + run engine. |
| 136 | +
|
| 137 | + Returns: |
| 138 | + A ``RunEngineResult`` instance. The return value of the plan can then be accessed using |
| 139 | + the ``plan_result`` attribute. |
| 140 | +
|
| 141 | + Raises: |
| 142 | + RuntimeError: if the run engine was not idle at the start of this call. |
| 143 | + RuntimeError: if a reentrant call to the run engine is detected. |
| 144 | + :py:obj:`bluesky.utils.RunEngineInterrupted`: if the user, or the plan itself, explicitly |
| 145 | + requests an interruption. |
| 146 | +
|
| 147 | + Calling a plan using this function means that keyboard-interrupt handling will be |
| 148 | + degraded: all keyboard interrupts will now force an immediate abort of the plan, using |
| 149 | + ``RE.abort()``, rather than giving the possibility of gracefully resuming. Cleanup handlers |
| 150 | + will execute during the ``RE.abort()``. |
| 151 | +
|
| 152 | + The bluesky run engine is not reentrant. It is a programming error to attempt to run a plan |
| 153 | + using this function, from within a plan. To call a sub plan from within an outer plan, use:: |
| 154 | +
|
| 155 | + def outer_plan(): |
| 156 | + ... |
| 157 | + yield from subplan(...) |
| 158 | +
|
| 159 | + """ |
| 160 | + RE = get_run_engine() |
| 161 | + |
| 162 | + if not _RUN_PLAN_LOCK.acquire(blocking=False): |
| 163 | + raise RuntimeError( |
| 164 | + "reentrant run_plan call attempted; this cannot be supported.\n" |
| 165 | + "It is a programming error to attempt to run a plan using run_plan " |
| 166 | + "from within a plan.\n" |
| 167 | + "To call a sub plan from within an outer plan, " |
| 168 | + "use 'yield from subplan(...)' instead.\n" |
| 169 | + ) |
| 170 | + try: |
| 171 | + if RE.state != "idle": |
| 172 | + raise RuntimeError( |
| 173 | + "Cannot run plan; RunEngine is not idle at start of run_plan call. " |
| 174 | + "You may need to call RE.abort() to abort after a previous plan." |
| 175 | + ) |
| 176 | + try: |
| 177 | + return cast(RunEngineResult, RE(plan, **metadata_kw)) |
| 178 | + except (RunEngineInterrupted, RunEngineControlException): |
| 179 | + raise RunEngineInterrupted( |
| 180 | + "bluesky RunEngine interrupted; not resumable as running via run_plan" |
| 181 | + ) from None |
| 182 | + finally: |
| 183 | + if RE.state != "idle": |
| 184 | + # Safest reasonable default? Don't want to halt() |
| 185 | + # as that wouldn't do cleanup e.g. dae settings |
| 186 | + RE.abort() |
| 187 | + finally: |
| 188 | + _RUN_PLAN_LOCK.release() |
0 commit comments