diff --git a/src/openflexure_microscope_server/stitching.py b/src/openflexure_microscope_server/stitching.py index b1a88863..e87afe1f 100644 --- a/src/openflexure_microscope_server/stitching.py +++ b/src/openflexure_microscope_server/stitching.py @@ -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() diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 0ee5880b..b4ed6c10 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -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)