From f794559be9dcd49725e2eed19a5a5ae5e3517e8d Mon Sep 17 00:00:00 2001 From: Henry Pinkard <7969470+henrypinkard@users.noreply.github.com> Date: Thu, 21 Sep 2023 15:52:42 -0700 Subject: [PATCH 1/4] fix shutdown behavior when calling stop_headless --- pycromanager/_version.py | 2 +- .../acquisition/java_backend_acquisitions.py | 2 +- pycromanager/headless.py | 10 ++++--- .../zmq_bridge/{_bridge.py => bridge.py} | 27 ++++++++++++++----- pycromanager/zmq_bridge/wrappers.py | 2 +- 5 files changed, 31 insertions(+), 12 deletions(-) rename pycromanager/zmq_bridge/{_bridge.py => bridge.py} (97%) diff --git a/pycromanager/_version.py b/pycromanager/_version.py index b6675a0a..dae0f87c 100644 --- a/pycromanager/_version.py +++ b/pycromanager/_version.py @@ -1,2 +1,2 @@ -version_info = (0, 29, 1) +version_info = (0, 29, 2) __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..771a39e9 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 @@ -179,7 +181,6 @@ 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", @@ -189,6 +190,7 @@ def start_headless( 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..13cbaa32 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( @@ -562,15 +571,21 @@ def _close(self): "hash-code": self._hash_code, "java_class_name": self._java_class # for debugging } + print('sending message') 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 + print('got 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 From 30529f67eed2f1b4180704b706f6243169f68dfe Mon Sep 17 00:00:00 2001 From: Henry Pinkard <7969470+henrypinkard@users.noreply.github.com> Date: Thu, 21 Sep 2023 15:54:46 -0700 Subject: [PATCH 2/4] bump version --- pycromanager/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycromanager/_version.py b/pycromanager/_version.py index dae0f87c..e3a50685 100644 --- a/pycromanager/_version.py +++ b/pycromanager/_version.py @@ -1,2 +1,2 @@ -version_info = (0, 29, 2) +version_info = (0, 29, 3) __version__ = ".".join(map(str, version_info)) From fde009791dfbbad51f6cdbf68acae8e1a65cc884 Mon Sep 17 00:00:00 2001 From: Henry Pinkard <7969470+henrypinkard@users.noreply.github.com> Date: Thu, 21 Sep 2023 16:03:37 -0700 Subject: [PATCH 3/4] remove prints --- pycromanager/_version.py | 2 +- pycromanager/zmq_bridge/bridge.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pycromanager/_version.py b/pycromanager/_version.py index e3a50685..ffb96782 100644 --- a/pycromanager/_version.py +++ b/pycromanager/_version.py @@ -1,2 +1,2 @@ -version_info = (0, 29, 3) +version_info = (0, 29, 4) __version__ = ".".join(map(str, version_info)) diff --git a/pycromanager/zmq_bridge/bridge.py b/pycromanager/zmq_bridge/bridge.py index 13cbaa32..ecb835d7 100644 --- a/pycromanager/zmq_bridge/bridge.py +++ b/pycromanager/zmq_bridge/bridge.py @@ -571,14 +571,12 @@ def _close(self): "hash-code": self._hash_code, "java_class_name": self._java_class # for debugging } - print('sending message') self._send(message) 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 - print('got reply') if reply_json is not None and reply_json["type"] == "exception": raise Exception(reply_json["value"]) self._closed = True From b42e7d4138ecca1e51807350f058f2ddcb19af08 Mon Sep 17 00:00:00 2001 From: Henry Pinkard <7969470+henrypinkard@users.noreply.github.com> Date: Fri, 22 Sep 2023 07:55:23 -0700 Subject: [PATCH 4/4] fix headless mode with no config file --- pycromanager/_version.py | 2 +- pycromanager/headless.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pycromanager/_version.py b/pycromanager/_version.py index ffb96782..fc883cc8 100644 --- a/pycromanager/_version.py +++ b/pycromanager/_version.py @@ -1,2 +1,2 @@ -version_info = (0, 29, 4) +version_info = (0, 29, 5) __version__ = ".".join(map(str, version_info)) diff --git a/pycromanager/headless.py b/pycromanager/headless.py index 771a39e9..2601f9fb 100644 --- a/pycromanager/headless.py +++ b/pycromanager/headless.py @@ -119,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): @@ -185,7 +185,7 @@ def start_headless( # "-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