2525import sys
2626import time
2727import traceback
28+ from concurrent .futures import Future
2829from contextlib import ExitStack
2930from datetime import datetime
3031from typing import TYPE_CHECKING
7576from datalab .gui .panel import base , history , image , macro , signal
7677from datalab .gui .plugins .applications import ApplicationsDialog , get_plugin_icon
7778from datalab .gui .plugins .config import PluginConfigDialog
78- from datalab .gui .plugins .instrument import InstrumentWindow
79+ from datalab .gui .plugins .instrument import (
80+ InstrumentWindow ,
81+ SerialWorker ,
82+ wait_instruments_closed ,
83+ )
7984from datalab .gui .processor .base import FeatureNotFoundError
8085from datalab .gui .processor .preview import PreviewExecutorCache
8186from datalab .gui .settings import AI_OPTION_NAMES , edit_settings
@@ -172,6 +177,10 @@ def __init__(self, console=None, hide_on_close=False):
172177 self .aiassistantpanel = None # type: ignore[assignment]
173178 self .__applications_dialog : ApplicationsDialog | None = None
174179 self .__instrument_windows : dict [tuple [str , str ], InstrumentWindow ] = {}
180+ #: Worker of each plugin instrument, shared by its successive windows
181+ self .__instrument_workers : dict [tuple [str , str ], SerialWorker ] = {}
182+ #: Title and closing call of the last closed window of each instrument
183+ self .__instrument_closings : dict [tuple [str , str ], tuple [str , Future ]] = {}
175184 self .welcomepanel : WelcomePanel | None = None
176185
177186 self .signalpanel_toolbar : QW .QToolBar | None = None
@@ -509,15 +518,18 @@ def add_group(
509518 title : str ,
510519 panel : Literal ["signal" , "image" ] | None = None ,
511520 select : bool = False ,
512- ) -> None :
521+ ) -> str :
513522 """Add group to DataLab.
514523
515524 Args:
516525 title: Group title
517526 panel: Panel name. Defaults to None.
518527 select: Select the group after creation. Defaults to False.
528+
529+ Returns:
530+ UUID of the created group
519531 """
520- self .__get_datapanel (panel ).add_group (title , select )
532+ return get_uuid ( self .__get_datapanel (panel ).add_group (title , select ) )
521533
522534 @remote_controlled
523535 def select_objects (
@@ -674,7 +686,9 @@ def call_method_slot(
674686 self .remote_server .exception = exc
675687
676688 @remote_controlled
677- def run_remote_command (self , command : Callable [..., object ], * args ) -> None :
689+ def run_remote_command (
690+ self , command : Callable [..., object ], * args , return_result : bool = False
691+ ) -> None :
678692 """Slot to run a command requested by a remote client in GUI thread.
679693
680694 The exception raised by the command, if any, is stored in RemoteServer to
@@ -684,9 +698,13 @@ def run_remote_command(self, command: Callable[..., object], *args) -> None:
684698 Args:
685699 command: Main window method running the command
686700 args: Command arguments
701+ return_result: If True, store the command result in RemoteServer, to be
702+ returned to the client (it must be XML-RPC serializable)
687703 """
688704 try :
689- command (* args )
705+ result = command (* args )
706+ if return_result :
707+ self .remote_server .result = result
690708 except Exception as exc : # pylint: disable=broad-except
691709 self .remote_server .exception = exc
692710
@@ -1143,11 +1161,24 @@ def __launch_plugin_tool(self, plugin: PluginBase, tool_id: str) -> None:
11431161 def open_plugin_instrument (
11441162 self , plugin : PluginBase , tool : PluginTool
11451163 ) -> InstrumentWindow :
1146- """Show the instrument window of a plugin tool, creating it if needed."""
1164+ """Show the instrument window of a plugin tool, creating it if needed.
1165+
1166+ The successive windows of an instrument share its worker: a window
1167+ opened again calls the instrument once the closed one has closed it.
1168+ """
11471169 key = (plugin .plugin_id , tool .id )
11481170 window = self .__instrument_windows .get (key )
1171+ if window is not None and window .close_future is not None :
1172+ # Closed while hidden: Qt did not emit `finished`, release it now
1173+ self .__forget_instrument_window (key )
1174+ window = None
11491175 if window is None :
1150- window = InstrumentWindow (self , plugin .get_instrument (tool .id ), tool .title )
1176+ worker = self .__instrument_workers .get (key )
1177+ if worker is None :
1178+ worker = self .__instrument_workers [key ] = SerialWorker ()
1179+ window = InstrumentWindow (
1180+ self , plugin .get_instrument (tool .id ), tool .title , worker
1181+ )
11511182 window .finished .connect (
11521183 functools .partial (self .__forget_instrument_window , key )
11531184 )
@@ -1158,15 +1189,33 @@ def open_plugin_instrument(
11581189 return window
11591190
11601191 def __forget_instrument_window (self , key : tuple [str , str ], * _args ) -> None :
1161- """Release a closed instrument window."""
1192+ """Release a closed instrument window, keeping track of its instrument
1193+ until it is closed."""
11621194 window = self .__instrument_windows .pop (key , None )
11631195 if window is not None :
1196+ window .stop ()
1197+ closing = (window .windowTitle (), window .close_future )
1198+ self .__instrument_closings [key ] = closing
11641199 window .deleteLater ()
11651200
11661201 def close_plugin_instruments (self ) -> None :
1167- """Close all plugin instrument windows."""
1202+ """Close all plugin instrument windows, and wait for their instruments,
1203+ including those of windows closed before, within a common time limit."""
11681204 for window in list (self .__instrument_windows .values ()):
11691205 window .close ()
1206+ # Windows that were not visible do not emit `finished` when closed
1207+ for key in list (self .__instrument_windows ):
1208+ self .__forget_instrument_window (key )
1209+ timeout = InstrumentWindow .CLOSE_TIMEOUT
1210+ closings = self .__instrument_closings .values ()
1211+ for title in wait_instruments_closed (closings , timeout ):
1212+ logging .getLogger (__name__ ).warning (
1213+ "Instrument '%s' did not close within %g s" , title , timeout
1214+ )
1215+ for worker in self .__instrument_workers .values ():
1216+ worker .shutdown ()
1217+ self .__instrument_workers .clear ()
1218+ self .__instrument_closings .clear ()
11701219
11711220 def __unregister_plugins (self ) -> None :
11721221 """Unregister all plugins and let them cleanup their hooks"""
@@ -2272,7 +2321,10 @@ def open_h5_files(
22722321 self ,
22732322 _ ("Open" ),
22742323 Conf .base_dir .get (),
2275- _ ("HDF5 files (*.h5 *.hdf5 *.hdf *.he5);;All files (*)" ),
2324+ _ (
2325+ "HDF5 files (*.h5 *.hdf5 *.hdf *.he5 *.nxs *.nx5);;"
2326+ "All files (*)"
2327+ ),
22762328 )
22772329 if not h5files :
22782330 return
0 commit comments