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:
commit
af93db7de8
4 changed files with 52 additions and 13 deletions
|
|
@ -9,6 +9,7 @@ Interpreter Lock (GIL). May be possible to shift to multiprocessing in the futur
|
||||||
from typing import Optional, Any
|
from typing import Optional, Any
|
||||||
import threading
|
import threading
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import signal
|
||||||
import os
|
import os
|
||||||
from io import TextIOWrapper
|
from io import TextIOWrapper
|
||||||
import shlex
|
import shlex
|
||||||
|
|
@ -160,11 +161,19 @@ class PreviewStitcher(BaseStitcher):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def wait(self) -> None:
|
def wait(self, cancel: lt.deps.CancelHook) -> None:
|
||||||
"""Wait for this preview stitch to return."""
|
"""Wait for this preview stitch to return.
|
||||||
if self.running:
|
|
||||||
with self._popen_lock:
|
:raises InvocationCancelledError: if the action is cancelled.
|
||||||
self._popen_obj.wait()
|
"""
|
||||||
|
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):
|
class FinalStitcher(BaseStitcher):
|
||||||
|
|
@ -272,10 +281,8 @@ class FinalStitcher(BaseStitcher):
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Run the final stitch logging any output.
|
"""Run the final stitch logging any output.
|
||||||
|
|
||||||
Raises:
|
:raises ChildProcessError: if exit code is not zero
|
||||||
ChildProcessError if exit code is not zero
|
:raises InvocationCancelledError: if the action is cancelled.
|
||||||
InvocationCancelledError if the action is cancelled.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
cmd = self.command
|
cmd = self.command
|
||||||
self.logger.debug(f"Running command in subprocess: `{' '.join(cmd)}`")
|
self.logger.debug(f"Running command in subprocess: `{' '.join(cmd)}`")
|
||||||
|
|
|
||||||
|
|
@ -466,7 +466,7 @@ class SmartScanThing(lt.Thing):
|
||||||
self._scan_logger.info("Waiting for background processes to finish...")
|
self._scan_logger.info("Waiting for background processes to finish...")
|
||||||
|
|
||||||
if self._preview_stitcher is not None:
|
if self._preview_stitcher is not None:
|
||||||
self._preview_stitcher.wait()
|
self._preview_stitcher.wait(self._cancel)
|
||||||
|
|
||||||
if self._scan_data.stitch_automatically:
|
if self._scan_data.stitch_automatically:
|
||||||
self._scan_logger.info("Stitching final image (may take some time)...")
|
self._scan_logger.info("Stitching final image (may take some time)...")
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ def main():
|
||||||
# This is used to check we catch errors correctly.
|
# This is used to check we catch errors correctly.
|
||||||
if arg == "ERROR":
|
if arg == "ERROR":
|
||||||
raise RuntimeError("I was told to do this.")
|
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)
|
print(arg, flush=True)
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import os
|
||||||
from copy import copy
|
from copy import copy
|
||||||
import uuid
|
import uuid
|
||||||
import threading
|
import threading
|
||||||
from time import sleep
|
import time
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
@ -200,6 +200,7 @@ def test_preview_stitching_command(caplog, mocker):
|
||||||
mocker.patch("openflexure_microscope_server.stitching.STITCHING_CMD", mock_cmd)
|
mocker.patch("openflexure_microscope_server.stitching.STITCHING_CMD", mock_cmd)
|
||||||
|
|
||||||
with caplog.at_level(logging.INFO):
|
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 = PreviewStitcher(FAKE_DIR, overlap=0.1, correlation_resize=0.5)
|
||||||
stitcher.start()
|
stitcher.start()
|
||||||
# Should take a second or so to run so will still be running
|
# 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):
|
with pytest.raises(RuntimeError):
|
||||||
stitcher.start()
|
stitcher.start()
|
||||||
# Wait for it to complete
|
# Wait for it to complete
|
||||||
stitcher.wait()
|
stitcher.wait(cancel_hook)
|
||||||
# It is now not running
|
# It is now not running
|
||||||
assert not stitcher.running
|
assert not stitcher.running
|
||||||
assert len(caplog.records) == 0
|
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):
|
def test_final_stitching_command(caplog, mocker):
|
||||||
"""Check the final stitch runs until completion, and print statements are logged."""
|
"""Check the final stitch runs until completion, and print statements are logged."""
|
||||||
mock_cmd = f"python {MOCK_STITCHER}"
|
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 = threading.Thread(target=stitcher.run, kwargs={"cancel": cancel_hook})
|
||||||
thread.start()
|
thread.start()
|
||||||
# Sleep long enough for at least 1 log.
|
# Sleep long enough for at least 1 log.
|
||||||
sleep(0.5)
|
time.sleep(0.5)
|
||||||
# use set() to cancel!
|
# use set() to cancel!
|
||||||
cancel_hook.set()
|
cancel_hook.set()
|
||||||
thread.join()
|
thread.join()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue