fix: release Micro-Manager devices on interpreter exit#8
Open
hinderling wants to merge 1 commit into
Open
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
atexithook inPyMMCoreMicroscope.__init__that cancels any running MDA and callsmmc.unloadAllDevices()when the Python interpreter shuts down. Hook is keyed on aweakrefso it doesn't pin the microscope instance._teardown_hardware()method on the base class. Subclasses with extra cleanup (background threads, serial ports, etc.) override it and chain tosuper()._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 thewakeup_dmdthread before the base unload, to avoid racing the thread's nextdisplaySLMImagecall.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.
PyMMCoreMicroscopeis 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:
python.exeloadsTiMoench.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).mosaic3_loaded=True, nodevice in useerror.Without this fix, Phase B would hang or raise on Mosaic 3 acquisition.