Skip to content

fix: release Micro-Manager devices on interpreter exit#8

Open
hinderling wants to merge 1 commit into
pertzlab:mainfrom
hinderling:fix/pymmcore-microscope-atexit-unload
Open

fix: release Micro-Manager devices on interpreter exit#8
hinderling wants to merge 1 commit into
pertzlab:mainfrom
hinderling:fix/pymmcore-microscope-atexit-unload

Conversation

@hinderling
Copy link
Copy Markdown
Collaborator

@hinderling hinderling commented May 14, 2026

Summary

  • Register an atexit hook in PyMMCoreMicroscope.__init__ that cancels any running MDA and calls mmc.unloadAllDevices() when the Python interpreter shuts down. Hook is keyed on a weakref so it doesn't pin the microscope instance.
  • Introduce an overridable _teardown_hardware() method on the base class. Subclasses with extra cleanup (background threads, serial ports, etc.) override it and chain to super()._teardown_hardware() last.
  • Moench.shutdown() now delegates to _teardown_hardware() so the explicit API and the atexit path run the same code. Moench._teardown_hardware() stops the wakeup_dmd thread before the base unload, to avoid racing the thread's next displaySLMImage call.

Why

Without explicit unload, device adapters stay bound to the dying Python process and the next process trying to load the same Micro-Manager configuration fails to acquire them. On the Moench rig this manifests as the Andor Mosaic 3 DMD refusing to load after any kernel restart that didn't go through mic.shutdown() (a script crash, a force-killed Jupyter kernel, etc.).

pymmcore-plus historically did this in atexit itself but removed it in pymmcore-plus#572, so cleanup is now the caller's responsibility. PyMMCoreMicroscope is the right layer for FARO — every subclass (Moench, Jungfrau, Niesen) gets the protection in one place.

Verification

Tested end-to-end on the Moench rig:

  • Phase A: fresh python.exe loads TiMoench.cfg, registers the hook, exits naturally. Atexit confirmed firing via the call chain visible in the exit-time logging trace (_atexit_teardown → _teardown_hardware → unloadAllDevices).
  • Phase B: separate brand-new process spawned after Phase A had fully exited. Loaded the same config back-to-back; Mosaic 3 acquired cleanly with mosaic3_loaded=True, no device in use error.

Without this fix, Phase B would hang or raise on Mosaic 3 acquisition.

Add an atexit hook in PyMMCoreMicroscope that cancels any running MDA
and calls mmc.unloadAllDevices() when the interpreter shuts down.
Without this, device adapters can stay bound to the dying Python
process and the next process trying to load the same configuration
fails to acquire them.

Subclasses with extra teardown (background threads, serial ports, etc.)
should override _teardown_hardware() and chain to super() last, so
subclass-owned resources are released before the device unload.

Moench.shutdown() now delegates to _teardown_hardware() so explicit
shutdown and the atexit path run the same code. The wakeup_dmd thread
is stopped in Moench._teardown_hardware() before the base unload, to
avoid racing the thread's next displaySLMImage call.
@hinderling hinderling requested a review from alandolt May 14, 2026 14:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant