wip
This commit is contained in:
@@ -18,8 +18,12 @@ from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
|
||||
from .schedule_item import ScheduleItem, ScheduleItemStatus
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
def protected_prefix_length(items: list[ScheduleItem]) -> int:
|
||||
"""How many items, from the start, are no longer PENDING."""
|
||||
@@ -41,7 +45,9 @@ def index_of(items: list[ScheduleItem], item_id: str | None) -> int | None:
|
||||
return None
|
||||
|
||||
|
||||
def pick_next_runnable(items: list[ScheduleItem]) -> ScheduleItem | Literal["stop"] | None:
|
||||
def pick_next_runnable(
|
||||
items: list[ScheduleItem], repeat_aborted_item
|
||||
) -> ScheduleItem | Literal["stop"] | None:
|
||||
"""
|
||||
What the execution loop should do next: always re-derived from scratch
|
||||
(never a remembered index/object) so edits made to the PENDING suffix
|
||||
@@ -51,10 +57,15 @@ def pick_next_runnable(items: list[ScheduleItem]) -> ScheduleItem | Literal["sto
|
||||
earlier item failed/was aborted (execution stays parked there until the
|
||||
operator intervenes), or `None` if every item is COMPLETED.
|
||||
"""
|
||||
logger.info(f"pick next runnable, repeat abort item is {repeat_aborted_item}")
|
||||
for item in items:
|
||||
logger.info(f"item: {item}")
|
||||
if item.status == ScheduleItemStatus.COMPLETED:
|
||||
continue
|
||||
if item.status in (ScheduleItemStatus.FAILED, ScheduleItemStatus.ABORTED):
|
||||
if item.status == ScheduleItemStatus.ABORTED:
|
||||
if not repeat_aborted_item:
|
||||
continue
|
||||
if item.status == ScheduleItemStatus.FAILED:
|
||||
return "stop"
|
||||
return item # PENDING, or RUNNING (reconciled as still active)
|
||||
return item # PENDING, RUNNING or ABORTED if repeat_aborted_item is set (reconciled as still active)
|
||||
return None
|
||||
|
||||
@@ -64,21 +64,15 @@ DEFAULT_COMMANDS = [
|
||||
_ACTIVE_QUEUE_STATES = ("PENDING", "RUNNING")
|
||||
_ITEM_ID_ROLE = Qt.UserRole + 1 # QListWidgetItem data role used to map a row back to an item_id
|
||||
|
||||
_STATUS_ICON = {
|
||||
ScheduleItemStatus.PENDING: "\u23f3", # hourglass
|
||||
ScheduleItemStatus.RUNNING: "\u25b6", # play
|
||||
ScheduleItemStatus.COMPLETED: "\u2705", # check mark
|
||||
ScheduleItemStatus.FAILED: "\u274c", # cross mark
|
||||
ScheduleItemStatus.ABORTED: "\u23f9", # stop
|
||||
}
|
||||
|
||||
# TODO: Colors should change when the item is selected, otherwise won't be readable
|
||||
ICON_SIZE = 20
|
||||
_ICON_MAP = {
|
||||
ScheduleItemStatus.PENDING: ("hourglass", "#919090"),
|
||||
ScheduleItemStatus.RUNNING: ("cycle", "#2980b9"),
|
||||
ScheduleItemStatus.COMPLETED: ("check", "#27ae60"),
|
||||
ScheduleItemStatus.FAILED: ("warning", "#e74c3c"),
|
||||
ScheduleItemStatus.ABORTED: ("cancel", "#e74c3c"),
|
||||
ScheduleItemStatus.ABORTED: ("cancel", "#e6d922"),
|
||||
}
|
||||
|
||||
|
||||
@@ -224,6 +218,8 @@ class Scheduler(BECWidget, QWidget):
|
||||
# schedule (another instance of this widget, a script, ...).
|
||||
self.bec_dispatcher.connect_slot(self._on_remote_update, self._endpoint)
|
||||
|
||||
self._closing = threading.Event()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# UI
|
||||
# ------------------------------------------------------------------ #
|
||||
@@ -378,8 +374,7 @@ class Scheduler(BECWidget, QWidget):
|
||||
# again shortly); accepting a wholesale replacement here
|
||||
# would orphan those objects. See module docstring.
|
||||
logger.info(
|
||||
"Ignoring remote schedule update for '%s' while a local run is in progress.",
|
||||
self.schedule_name,
|
||||
f"Ignoring remote schedule update for {self.schedule_name} while a local run is in progress."
|
||||
)
|
||||
return
|
||||
self.schedule = new_schedule
|
||||
@@ -438,27 +433,16 @@ class Scheduler(BECWidget, QWidget):
|
||||
def _refresh_ui(self):
|
||||
with self._lock:
|
||||
items = list(self.schedule.items)
|
||||
is_running = self.schedule.is_running
|
||||
|
||||
self.list_widget.blockSignals(True)
|
||||
self.list_widget.clear()
|
||||
selected_row = None
|
||||
for row, item in enumerate(items):
|
||||
|
||||
# text = f"{_STATUS_ICON.get(item.status, '?')} {item.command}"
|
||||
|
||||
text = f"{item.command}"
|
||||
|
||||
icon_name, color = _ICON_MAP[item.status]
|
||||
icon = material_icon(
|
||||
icon_name, size=(ICON_SIZE, ICON_SIZE), color=color, convert_to_pixmap=True
|
||||
)
|
||||
# if item.status == ScheduleItemStatus.RUNNING:
|
||||
# self._spin_anim.start()
|
||||
# else:
|
||||
# self._spin_anim.stop()
|
||||
# self._label.setPixmap(icon)
|
||||
|
||||
if item.error:
|
||||
text += f" ({item.error.strip().splitlines()[-1]})"
|
||||
list_item = QListWidgetItem(text, self.list_widget)
|
||||
@@ -472,33 +456,6 @@ class Scheduler(BECWidget, QWidget):
|
||||
self._selected_item_id = None
|
||||
self.list_widget.blockSignals(False)
|
||||
|
||||
# idx = None
|
||||
# selected_item = None
|
||||
# if self._selected_item_id is not None:
|
||||
# for i, item in enumerate(items):
|
||||
# if item.item_id == self._selected_item_id:
|
||||
# idx, selected_item = i, item
|
||||
# break
|
||||
|
||||
# # Editable = the selected item hasn't started yet. Thanks to the
|
||||
# # protected-prefix invariant, everything after the first PENDING
|
||||
# # item is guaranteed PENDING too, so a plain index/status check is
|
||||
# # enough here - see module docstring.
|
||||
# can_edit_selected = (
|
||||
# selected_item is not None and selected_item.status == ScheduleItemStatus.PENDING
|
||||
# )
|
||||
|
||||
# self.run_btn.setEnabled(not is_running)
|
||||
# # Adding is always safe: new items are clamped into the PENDING
|
||||
# # suffix regardless of what's selected (see add_item()).
|
||||
# self.add_btn.setEnabled(True)
|
||||
# self.edit_btn.setEnabled(can_edit_selected)
|
||||
# self.delete_btn.setEnabled(can_edit_selected)
|
||||
# self.move_up_btn.setEnabled(can_edit_selected and idx not in (None, 0))
|
||||
# self.move_down_btn.setEnabled(
|
||||
# can_edit_selected and idx is not None and idx < len(items) - 1
|
||||
# )
|
||||
|
||||
if self._guard.enabled:
|
||||
state = "OK" if self._guard.is_clear() else "PAUSED - waiting to resume"
|
||||
value = self._guard.current_value
|
||||
@@ -540,7 +497,6 @@ class Scheduler(BECWidget, QWidget):
|
||||
|
||||
@SafeSlot()
|
||||
def _on_selection_changed(self, row: int):
|
||||
logger.info("On selection changed")
|
||||
item = self.list_widget.item(row) if row >= 0 else None
|
||||
self._selected_item_id = item.data(_ITEM_ID_ROLE) if item is not None else None
|
||||
self._update_buttons()
|
||||
@@ -695,12 +651,26 @@ class Scheduler(BECWidget, QWidget):
|
||||
with self._lock:
|
||||
if self.schedule.is_running:
|
||||
return
|
||||
repeat_aborted_item = False
|
||||
if any(item.status == ScheduleItemStatus.ABORTED for item in self.schedule.items):
|
||||
repeat_aborted_item = (
|
||||
QMessageBox.question(
|
||||
self,
|
||||
"Repeat aborted scan",
|
||||
"Would you like to repeat the aborted scan?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
)
|
||||
== QMessageBox.StandardButton.Yes
|
||||
)
|
||||
self._abort_requested = False
|
||||
self.schedule.is_running = True
|
||||
self._persist_locked()
|
||||
self.schedule_changed.emit()
|
||||
self.submit_task(
|
||||
self._run_all, on_complete=self._on_run_finished, on_failed=self._on_run_failed
|
||||
self._run_all,
|
||||
repeat_aborted_item,
|
||||
on_complete=self._on_run_finished,
|
||||
on_failed=self._on_run_failed,
|
||||
)
|
||||
|
||||
@SafeSlot()
|
||||
@@ -869,16 +839,16 @@ class Scheduler(BECWidget, QWidget):
|
||||
# the `schedule_changed` Qt signal so they are marshalled back onto
|
||||
# the GUI thread instead of touching widgets directly)
|
||||
# ------------------------------------------------------------------ #
|
||||
def _pick_next_runnable_locked(self):
|
||||
def _pick_next_runnable_locked(self, repeat_aborted_item):
|
||||
"""Must be called while holding `self._lock`; see `schedule_logic.pick_next_runnable`."""
|
||||
return pick_next_runnable(self.schedule.items)
|
||||
return pick_next_runnable(self.schedule.items, repeat_aborted_item)
|
||||
|
||||
def _run_all(self):
|
||||
def _run_all(self, repeat_aborted_item):
|
||||
logger.info("_run_all was called")
|
||||
namespace = {"scans": self.scans, "dev": self.dev}
|
||||
while not self._abort_requested:
|
||||
with self._lock:
|
||||
next_item = self._pick_next_runnable_locked()
|
||||
next_item = self._pick_next_runnable_locked(repeat_aborted_item)
|
||||
if next_item is None or next_item == "stop":
|
||||
logger.info("Next item is None or stop")
|
||||
break
|
||||
@@ -891,7 +861,7 @@ class Scheduler(BECWidget, QWidget):
|
||||
logger.info("in _run_all, set is_running to false")
|
||||
self.schedule.is_running = False
|
||||
self._persist_locked()
|
||||
self.schedule_changed.emit()
|
||||
self._emit_schedule_changed()
|
||||
|
||||
def _execute_item(self, item: ScheduleItem, namespace: dict):
|
||||
if item.kind == "scan":
|
||||
@@ -899,7 +869,7 @@ class Scheduler(BECWidget, QWidget):
|
||||
# the guard is clear or the operator aborts. See "Guard
|
||||
# interaction" in the module docstring.
|
||||
cleared = self._guard.wait_until_clear(should_abort=lambda: self._abort_requested)
|
||||
self.schedule_changed.emit()
|
||||
self._emit_schedule_changed()
|
||||
if not cleared:
|
||||
return
|
||||
|
||||
@@ -908,7 +878,7 @@ class Scheduler(BECWidget, QWidget):
|
||||
item.error = None
|
||||
item.started_at = time.time()
|
||||
self._persist_locked()
|
||||
self.schedule_changed.emit()
|
||||
self._emit_schedule_changed()
|
||||
|
||||
try:
|
||||
report = eval(
|
||||
@@ -964,7 +934,7 @@ class Scheduler(BECWidget, QWidget):
|
||||
final_status = item.status
|
||||
self._current_report = None
|
||||
self._current_item_kind = None
|
||||
self.schedule_changed.emit()
|
||||
self._emit_schedule_changed()
|
||||
|
||||
if final_status in (ScheduleItemStatus.COMPLETED, ScheduleItemStatus.FAILED):
|
||||
notify_item_finished(
|
||||
@@ -986,7 +956,7 @@ class Scheduler(BECWidget, QWidget):
|
||||
item.status = ScheduleItemStatus.COMPLETED
|
||||
item.finished_at = time.time()
|
||||
self._persist_locked()
|
||||
self.schedule_changed.emit()
|
||||
self._emit_schedule_changed()
|
||||
return
|
||||
time.sleep(0.5)
|
||||
|
||||
@@ -1002,13 +972,16 @@ class Scheduler(BECWidget, QWidget):
|
||||
self._persist_locked()
|
||||
self._refresh_ui()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# cleanup
|
||||
# ------------------------------------------------------------------ #
|
||||
def _emit_schedule_changed(self):
|
||||
if self._closing.is_set():
|
||||
return
|
||||
|
||||
self.schedule_changed.emit()
|
||||
|
||||
def cleanup(self):
|
||||
# Note: we deliberately do NOT abort a running schedule here. The
|
||||
# whole point of the Redis-backed design is that closing this
|
||||
# widget must not interrupt anything already submitted to the
|
||||
# scan/device server - see the module docstring.
|
||||
# The schedule itself must continue running after the widget is closed.
|
||||
# We only tell the worker that the UI/Qt object is no longer available.
|
||||
self._closing.set()
|
||||
|
||||
self._guard.cleanup()
|
||||
super().cleanup() # also disconnects all bec_dispatcher subscriptions for us
|
||||
super().cleanup()
|
||||
|
||||
Reference in New Issue
Block a user