diff --git a/pycromanager/_version.py b/pycromanager/_version.py index dae0f87c..fc883cc8 100644 --- a/pycromanager/_version.py +++ b/pycromanager/_version.py @@ -1,2 +1,2 @@ -version_info = (0, 29, 2) +version_info = (0, 29, 5) __version__ = ".".join(map(str, version_info)) diff --git a/pycromanager/acquisition/java_backend_acquisitions.py b/pycromanager/acquisition/java_backend_acquisitions.py index 67013717..98235619 100644 --- a/pycromanager/acquisition/java_backend_acquisitions.py +++ b/pycromanager/acquisition/java_backend_acquisitions.py @@ -9,7 +9,7 @@ import threading from inspect import signature import time -from pycromanager.zmq_bridge._bridge import deserialize_array +from pycromanager.zmq_bridge.bridge import deserialize_array from pycromanager.zmq_bridge.wrappers import PullSocket, PushSocket, JavaObject, JavaClass from pycromanager.zmq_bridge.wrappers import DEFAULT_BRIDGE_PORT as DEFAULT_PORT from pycromanager.mm_java_classes import ZMQRemoteMMCoreJ, Magellan diff --git a/pycromanager/headless.py b/pycromanager/headless.py index 12548e08..2601f9fb 100644 --- a/pycromanager/headless.py +++ b/pycromanager/headless.py @@ -5,7 +5,7 @@ import types from pycromanager.acquisition.acq_eng_py.internal.engine import Engine -from pycromanager.zmq_bridge._bridge import _Bridge +from pycromanager.zmq_bridge.bridge import _Bridge, server_terminated from pymmcore import CMMCore import pymmcore @@ -89,9 +89,11 @@ def get_tagged_image(self, cam_index, camera, height, width, binning=None, pixel def stop_headless(debug=False): for p in _JAVA_HEADLESS_SUBPROCESSES: + port = p.port if debug: print('Stopping headless process with pid {}'.format(p.pid)) p.terminate() + server_terminated(port) if debug: print('Waiting for process with pid {} to terminate'.format(p.pid)) p.wait() # wait for process to terminate @@ -117,7 +119,7 @@ def stop_headless(debug=False): atexit.register(stop_headless) def start_headless( - mm_app_path: str, config_file: str='', java_loc: str=None, + mm_app_path: str, config_file: str=None, java_loc: str=None, python_backend=False, core_log_path: str='', buffer_size_mb: int=1024, max_memory_mb: int=2000, port: int=_Bridge.DEFAULT_PORT, debug=False): @@ -179,16 +181,16 @@ def start_headless( classpath, "-Dsun.java2d.dpiaware=false", f"-Xmx{max_memory_mb}m", - # This is used by MM desktop app but breaks things on MacOS...Don't think its neccessary # "-XX:MaxDirectMemorySize=1000", "org.micromanager.remote.HeadlessLauncher", str(port), - config_file, + config_file if config_file is not None else '', str(buffer_size_mb), core_log_path, ], cwd=mm_app_path, stdout=subprocess.PIPE ) + process.port = port _JAVA_HEADLESS_SUBPROCESSES.append(process) started = False @@ -203,7 +205,9 @@ def start_headless( print('Headless mode started') def logger(): while process in _JAVA_HEADLESS_SUBPROCESSES: - print(process.stdout.readline().decode('utf-8')) + line = process.stdout.readline().decode('utf-8') + if line.strip() != '': + print(line) threading.Thread(target=logger).start() diff --git a/pycromanager/zmq_bridge/_bridge.py b/pycromanager/zmq_bridge/bridge.py similarity index 97% rename from pycromanager/zmq_bridge/_bridge.py rename to pycromanager/zmq_bridge/bridge.py index 4a469dd6..ecb835d7 100644 --- a/pycromanager/zmq_bridge/_bridge.py +++ b/pycromanager/zmq_bridge/bridge.py @@ -205,12 +205,19 @@ def close(self): print('closed socket {}'.format(self._port)) self._closed = True +def server_terminated(port): + """ + Call when the server on the Java side has been terminated. There is + no way to detect this directly due to the server-client architecture. + So this function will tell all JavaObjectShadow instances to not wait + around for a response from the server. + """ + _Bridge._ports_with_terminated_servers.add(port) class _Bridge: """ Create an object which acts as a client to a corresponding server (running in a Java process). - This enables construction and interaction with arbitrary java objects. Each bridge object should - be run using a context manager (i.e. `with Bridge() as b:`) or bridge.close() should be explicitly + This enables construction and interaction with arbitrary java objects. Bridge.close() should be explicitly called when finished """ @@ -220,6 +227,8 @@ class _Bridge: _bridge_creation_lock = threading.Lock() _cached_bridges_by_port_and_thread = {} + _ports_with_terminated_servers = set() + @staticmethod def create_or_get_existing_bridge(port: int=DEFAULT_PORT, convert_camel_case: bool=True, @@ -426,7 +435,7 @@ def _get_java_class(self, classpath: str, new_socket: bool=False, debug: bool=Fa if new_socket: # create a new bridge over a different port bridge = _Bridge.create_or_get_existing_bridge(port=serialized_object["port"], ip_address=self._ip_address, - timeout=self._timeout, debug=debug) + timeout=self._timeout, debug=debug) else: bridge = self return self._class_factory.create( @@ -563,14 +572,18 @@ def _close(self): "java_class_name": self._java_class # for debugging } self._send(message) - reply_json = self._receive() - - if reply_json["type"] == "exception": + reply_json = None + while reply_json is None: + reply_json = self._get_bridge()._receive(timeout=self._timeout) + if self._creation_port in _Bridge._ports_with_terminated_servers: + break # the server has been terminated, so we can't expect a reply + if reply_json is not None and reply_json["type"] == "exception": raise Exception(reply_json["value"]) self._closed = True # release references to bridges so they can be garbage collected # if unused by other objects self._bridges_by_port_thread = None + self._creation_bridge = None def _send(self, message): """ diff --git a/pycromanager/zmq_bridge/wrappers.py b/pycromanager/zmq_bridge/wrappers.py index bc952e25..4f92e668 100644 --- a/pycromanager/zmq_bridge/wrappers.py +++ b/pycromanager/zmq_bridge/wrappers.py @@ -1,7 +1,7 @@ """ These classes wrap the ZMQ backend for ease of access """ -from pycromanager.zmq_bridge._bridge import _JavaObjectShadow, _Bridge, _DataSocket +from pycromanager.zmq_bridge.bridge import _JavaObjectShadow, _Bridge, _DataSocket import zmq DEFAULT_BRIDGE_PORT = _Bridge.DEFAULT_PORT