Merge branch 'let-preview-stitch-cancel' into 'v3'

Allow preview to cancel!

See merge request openflexure/openflexure-microscope-server!359
This commit is contained in:
Julian Stirling 2025-08-13 15:18:13 +00:00
commit af93db7de8
4 changed files with 52 additions and 13 deletions

View file

@ -9,6 +9,7 @@ Interpreter Lock (GIL). May be possible to shift to multiprocessing in the futur
from typing import Optional, Any
import threading
import subprocess
import signal
import os
from io import TextIOWrapper
import shlex
@ -160,11 +161,19 @@ class PreviewStitcher(BaseStitcher):
return True
return False
def wait(self) -> None:
"""Wait for this preview stitch to return."""
if self.running:
with self._popen_lock:
self._popen_obj.wait()
def wait(self, cancel: lt.deps.CancelHook) -> None:
"""Wait for this preview stitch to return.
:raises InvocationCancelledError: if the action is cancelled.
"""
while self.running:
try:
cancel.sleep(0.1)
except lt.exceptions.InvocationCancelledError as e:
with self._popen_lock:
if self._popen_obj is not None:
self._popen_obj.send_signal(signal.SIGKILL)
raise (e)
class FinalStitcher(BaseStitcher):
@ -272,10 +281,8 @@ class FinalStitcher(BaseStitcher):
) -> None:
"""Run the final stitch logging any output.
Raises:
ChildProcessError if exit code is not zero
InvocationCancelledError if the action is cancelled.
:raises ChildProcessError: if exit code is not zero
:raises InvocationCancelledError: if the action is cancelled.
"""
cmd = self.command
self.logger.debug(f"Running command in subprocess: `{' '.join(cmd)}`")

View file

@ -466,7 +466,7 @@ class SmartScanThing(lt.Thing):
self._scan_logger.info("Waiting for background processes to finish...")
if self._preview_stitcher is not None:
self._preview_stitcher.wait()
self._preview_stitcher.wait(self._cancel)
if self._scan_data.stitch_automatically:
self._scan_logger.info("Stitching final image (may take some time)...")

View file

@ -11,6 +11,9 @@ def main():
# This is used to check we catch errors correctly.
if arg == "ERROR":
raise RuntimeError("I was told to do this.")
if arg == "HANG":
# Rather than hang, just sleep for 10s:
time.sleep(10)
print(arg, flush=True)
time.sleep(0.2)

View file

@ -9,7 +9,7 @@ import os
from copy import copy
import uuid
import threading
from time import sleep
import time
import pytest
@ -200,6 +200,7 @@ def test_preview_stitching_command(caplog, mocker):
mocker.patch("openflexure_microscope_server.stitching.STITCHING_CMD", mock_cmd)
with caplog.at_level(logging.INFO):
cancel_hook = lt.deps.CancelHook(id=uuid.uuid4())
stitcher = PreviewStitcher(FAKE_DIR, overlap=0.1, correlation_resize=0.5)
stitcher.start()
# Should take a second or so to run so will still be running
@ -208,12 +209,40 @@ def test_preview_stitching_command(caplog, mocker):
with pytest.raises(RuntimeError):
stitcher.start()
# Wait for it to complete
stitcher.wait()
stitcher.wait(cancel_hook)
# It is now not running
assert not stitcher.running
assert len(caplog.records) == 0
def test_preview_stitching_cancelled(caplog, mocker):
"""Check that preview stitch can be cancelled."""
mock_cmd = f"python {MOCK_STITCHER}"
mocker.patch("openflexure_microscope_server.stitching.STITCHING_CMD", mock_cmd)
with caplog.at_level(logging.INFO):
stitcher = PreviewStitcher(FAKE_DIR, overlap=0.1, correlation_resize=0.5)
# Send in the argument HANG to mock-stitch and it just hang for 10s
stitcher._extra_args = ["HANG"]
cancel_hook = lt.deps.CancelHook(id=uuid.uuid4())
t_start = time.time()
# Start stitching
stitcher.start()
# Run wait() in a thread.
thread = threading.Thread(target=stitcher.wait, kwargs={"cancel": cancel_hook})
thread.start()
# Sleep long enough for at least 1 log.
time.sleep(0.5)
# use set() to cancel!
cancel_hook.set()
thread.join()
# If it wasn't cancelled it would hang for 10 s. Here we check the cancel killed
# it within 2s.
assert t_start - time.time() < 2
assert len(caplog.records) == 0
def test_final_stitching_command(caplog, mocker):
"""Check the final stitch runs until completion, and print statements are logged."""
mock_cmd = f"python {MOCK_STITCHER}"
@ -252,7 +281,7 @@ def test_final_stitching_command_cancelled(caplog, mocker):
thread = threading.Thread(target=stitcher.run, kwargs={"cancel": cancel_hook})
thread.start()
# Sleep long enough for at least 1 log.
sleep(0.5)
time.sleep(0.5)
# use set() to cancel!
cancel_hook.set()
thread.join()