Remove all cancel hooks from smart scan and stitching

This commit is contained in:
Julian Stirling 2025-12-17 14:02:14 +00:00
parent 150e69d235
commit 510a44dd17
2 changed files with 19 additions and 32 deletions

View file

@ -6,6 +6,7 @@ CPU intensity of stitching causing scanning problems due to the Python Global
Interpreter Lock (GIL). May be possible to shift to multiprocessing in the future.
"""
import logging
import os
import shlex
import signal
@ -167,14 +168,17 @@ class PreviewStitcher(BaseStitcher):
return False
return self._popen_obj.poll() is None
def wait(self, cancel: lt.deps.CancelHook) -> None:
def wait(self) -> None:
"""Wait for this preview stitch to return.
:raises InvocationCancelledError: if the action is cancelled.
"""
while self.running:
try:
cancel.sleep(0.1)
# This should act exactly like sleep if not started in a LabThings
# action thread. In an action thread it will raise
# InvocationCancelledError if the action is cancelled.
lt.cancellable_sleep(0.2)
except lt.exceptions.InvocationCancelledError as e:
with self._popen_lock:
if self._popen_obj is not None:
@ -189,7 +193,7 @@ class FinalStitcher(BaseStitcher):
self,
images_dir: str,
*,
logger: lt.deps.InvocationLogger,
logger: logging.Logger,
overlap: Optional[float] = None,
correlation_resize: Optional[float] = None,
stitch_tiff: bool = False,
@ -200,7 +204,7 @@ class FinalStitcher(BaseStitcher):
All args except images_dir are positional only.
:param images_dir: The images directory of the scan to stitch.
:param logger: The invocation logger for this stitch process.
:param logger: The logger from the Thing that created this stitcher.
:param overlap: The scan overlap, if not known enter None. A value will be
chosen from the scan_data_dict, or set to a default value if no value is
available in the scan_data.
@ -281,10 +285,7 @@ class FinalStitcher(BaseStitcher):
)
return overlap, correlation_resize
def run(
self,
cancel: lt.deps.CancelHook,
) -> None:
def run(self) -> None:
"""Run the final stitch logging any output.
:raises ChildProcessError: if exit code is not zero
@ -305,7 +306,7 @@ class FinalStitcher(BaseStitcher):
# Stop opening pipe blocking writing to it
os.set_blocking(process.stdout.fileno(), False)
self._log_ongoing(process, cancel=cancel)
self._log_ongoing(process)
if process.poll() == 0:
self.logger.info("Stitching complete")
@ -319,11 +320,7 @@ class FinalStitcher(BaseStitcher):
f"Subprocess {STITCHING_CMD} errored with exit code {process.poll()}."
)
def _log_ongoing(
self,
process: subprocess.Popen,
cancel: lt.deps.CancelHook,
) -> None:
def _log_ongoing(self, process: subprocess.Popen) -> None:
"""Log the ongoing process unless it is cancelled."""
# Poll returns None while running, will return the error code when finished
while process.poll() is None:
@ -331,9 +328,10 @@ class FinalStitcher(BaseStitcher):
# Once buffer is clear sleep for 0.2s before trying again.
try:
# Note that using cancel.sleep allows the InvocationCancelledError to
# be thrown
cancel.sleep(0.2)
# This should act exactly like sleep if not started in a LabThings
# action thread. In an action thread it will raise
# InvocationCancelledError if the action is cancelled.
lt.cancellable_sleep(0.2)
except lt.exceptions.InvocationCancelledError as e:
self.logger.info("Stitching cancelled by user")
process.kill()

View file

@ -126,7 +126,6 @@ class SmartScanThing(lt.Thing):
# any method that calls these should be decorated with
# @_scan_running
self._scan_logger: Optional[lt.deps.InvocationLogger] = None
self._cancel: Optional[lt.deps.CancelHook] = None
self._autofocus: Optional[AutofocusDep] = None
self._stage: Optional[StageDep] = None
self._cam: Optional[CameraClient] = None
@ -139,7 +138,6 @@ class SmartScanThing(lt.Thing):
@lt.action
def sample_scan(
self,
cancel: lt.deps.CancelHook,
logger: lt.deps.InvocationLogger,
autofocus: AutofocusDep,
stage: StageDep,
@ -158,7 +156,6 @@ class SmartScanThing(lt.Thing):
raise RuntimeError("Trying to run scan while scan is already running!")
# Set private variables for this scan
self._cancel = cancel
self._scan_logger = logger
self._autofocus = autofocus
self._stage = stage
@ -188,7 +185,6 @@ class SmartScanThing(lt.Thing):
raise e
finally:
# However the scan finishes, unset all variables and release lock
self._cancel = None
self._scan_logger = None
self._autofocus = None
self._stage = None
@ -390,8 +386,6 @@ class SmartScanThing(lt.Thing):
self._save_final_scan_data(scan_result="success")
except lt.exceptions.InvocationCancelledError:
# Reset the cancel event so it can be thrown again
self._cancel.clear()
self._scan_logger.info("Stopping scan because it was cancelled.")
self._save_final_scan_data(scan_result="cancelled by user")
except Exception as e:
@ -513,13 +507,12 @@ 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._cancel)
self._preview_stitcher.wait()
if self._scan_data.stitch_automatically:
self._scan_logger.info("Stitching final image (may take some time)...")
self.stitch_scan(
logger=self._scan_logger,
cancel=self._cancel,
scan_name=self._ongoing_scan.name,
correlation_resize=self._scan_data.correlation_resize,
overlap=self._scan_data.overlap,
@ -731,7 +724,6 @@ class SmartScanThing(lt.Thing):
def stitch_scan(
self,
logger: lt.deps.InvocationLogger,
cancel: lt.deps.CancelHook,
scan_name: str,
correlation_resize: Optional[float] = None,
overlap: Optional[float] = None,
@ -753,8 +745,7 @@ class SmartScanThing(lt.Thing):
scan_data_dict=scan_data_dict,
)
try:
# start the final stitch, providing the cancel hook to allow aborting
final_stitcher.run(cancel)
final_stitcher.run()
except lt.exceptions.InvocationCancelledError:
# Sleep for 1 second just to allow invocation logs to pass to user.
time.sleep(1)
@ -774,9 +765,7 @@ class SmartScanThing(lt.Thing):
return ZipBlob.from_file(zip_fname)
@lt.action
def stitch_all_scans(
self, logger: lt.deps.InvocationLogger, cancel: lt.deps.CancelHook
) -> None:
def stitch_all_scans(self, logger: lt.deps.InvocationLogger) -> None:
"""Check the list of scans, and stitch any that don't have a DZI associated with it.
:raises RuntimeError: if the microscope is currently running a scan
@ -786,4 +775,4 @@ class SmartScanThing(lt.Thing):
raise RuntimeError("Can't stitch previous scans while a scan is ongoing")
for scan in self._get_all_scan_info():
if scan.dzi is None:
self.stitch_scan(logger=logger, cancel=cancel, scan_name=scan.name)
self.stitch_scan(logger=logger, scan_name=scan.name)