applets: basic embedding OK

This commit is contained in:
Sebastien Bourdeauducq 2016-02-08 09:59:15 +01:00
parent 70a67a0e38
commit 741b11c26d
3 changed files with 197 additions and 162 deletions

View File

@ -4,7 +4,25 @@ import asyncio
from quamash import QEventLoop, QtWidgets, QtGui, QtCore from quamash import QEventLoop, QtWidgets, QtGui, QtCore
from artiq.protocols.sync_struct import Subscriber from artiq.protocols.sync_struct import Subscriber
from artiq.protocols.pc_rpc import Client from artiq.protocols import pyon
from artiq.protocols.pipe_ipc import AsyncioChildComm
class AppletIPCClient(AsyncioChildComm):
def write_pyon(self, obj):
self.write(pyon.encode(obj).encode() + b"\n")
async def read_pyon(self):
line = await self.readline()
return pyon.decode(line.decode())
async def embed(self, win_id):
self.write_pyon({"action": "embed",
"win_id": win_id})
reply = await self.read_pyon()
if reply["action"] != "embed_done":
raise ValueError("Got erroneous reply to embed request",
reply)
class SimpleApplet: class SimpleApplet:
@ -13,27 +31,31 @@ class SimpleApplet:
self.main_widget_class = main_widget_class self.main_widget_class = main_widget_class
self.argparser = argparse.ArgumentParser(description=cmd_description) self.argparser = argparse.ArgumentParser(description=cmd_description)
self.argparser.add_argument("--update-delay", type=float, self.argparser.add_argument("--update-delay", type=float,
default=default_update_delay, default=default_update_delay,
help="time to wait after a mod (buffering other mods) " help="time to wait after a mod (buffering other mods) "
"before updating (default: %(default).2f)") "before updating (default: %(default).2f)")
group = self.argparser.add_argument_group("data server")
group.add_argument(
"--server-notify", default="::1",
help="hostname or IP to connect to for dataset notifications")
group.add_argument(
"--port-notify", default=3250, type=int,
help="TCP port to connect to for dataset notifications")
group = self.argparser.add_argument_group("GUI server")
group.add_argument(
"--server-gui", default="::1",
help="hostname or IP to connect to for GUI control")
group.add_argument(
"--port-gui", default=6501, type=int,
help="TCP port to connect to for GUI control")
group.add_argument("--embed", default=None, type=int,
help="embed main widget into existing window")
self._arggroup_datasets = self.argparser.add_argument_group("datasets") self._arggroup_datasets = self.argparser.add_argument_group("datasets")
subparsers = self.argparser.add_subparsers(dest="mode")
subparsers.required = True
parser_sa = subparsers.add_parser("standalone",
help="run standalone, connect to master directly")
parser_sa.add_argument(
"--server", default="::1",
help="hostname or IP to connect to")
parser_sa.add_argument(
"--port", default=3250, type=int,
help="TCP port to connect to")
parser_em = subparsers.add_parser("embedded",
help="embed into GUI")
parser_em.add_argument("ipc_address",
help="address for pipe_ipc")
self.dataset_args = set() self.dataset_args = set()
def add_dataset(self, name, help=None, required=True): def add_dataset(self, name, help=None, required=True):
@ -56,6 +78,25 @@ class SimpleApplet:
self.loop = QEventLoop(app) self.loop = QEventLoop(app)
asyncio.set_event_loop(self.loop) asyncio.set_event_loop(self.loop)
def ipc_init(self):
if self.args.mode == "standalone":
# nothing to do
pass
elif self.args.mode == "embedded":
self.ipc = AppletIPCClient(self.args.ipc_address)
self.loop.run_until_complete(self.ipc.connect())
else:
raise NotImplementedError
def ipc_close(self):
if self.args.mode == "standalone":
# nothing to do
pass
elif self.args.mode == "embedded":
self.ipc.close()
else:
raise NotImplementedError
def create_main_widget(self): def create_main_widget(self):
self.main_widget = self.main_widget_class(self.args) self.main_widget = self.main_widget_class(self.args)
# Qt window embedding is ridiculously buggy, and empirical testing # Qt window embedding is ridiculously buggy, and empirical testing
@ -65,15 +106,10 @@ class SimpleApplet:
# 3. applet sends the ID to host, host embeds the widget # 3. applet sends the ID to host, host embeds the widget
# 4. applet shows the widget # 4. applet shows the widget
# Doing embedding the other way around (using QWindow.setParent in the # Doing embedding the other way around (using QWindow.setParent in the
# applet) breaks resizing; furthermore the host needs to know our # applet) breaks resizing.
# window ID to request graceful termination by closing the window. if self.args.mode == "embedded":
if self.args.embed is not None:
win_id = int(self.main_widget.winId()) win_id = int(self.main_widget.winId())
remote = Client(self.args.server_gui, self.args.port_gui, "applets") self.loop.run_until_complete(self.ipc.embed(win_id))
try:
remote.embed(self.args.embed, win_id)
finally:
remote.close_rpc()
self.main_widget.show() self.main_widget.show()
def sub_init(self, data): def sub_init(self, data):
@ -81,6 +117,10 @@ class SimpleApplet:
return data return data
def filter_mod(self, mod): def filter_mod(self, mod):
if self.args.mode == "embedded":
# the parent already filters for us
return True
if mod["action"] == "init": if mod["action"] == "init":
return True return True
if mod["path"]: if mod["path"]:
@ -108,21 +148,40 @@ class SimpleApplet:
else: else:
self.main_widget.data_changed(self.data, [mod]) self.main_widget.data_changed(self.data, [mod])
def create_subscriber(self): def subscribe(self):
self.subscriber = Subscriber("datasets", if self.args.mode == "standalone":
self.sub_init, self.sub_mod) self.subscriber = Subscriber("datasets",
self.loop.run_until_complete(self.subscriber.connect( self.sub_init, self.sub_mod)
self.args.server_notify, self.args.port_notify)) self.loop.run_until_complete(self.subscriber.connect(
self.args.server_notify, self.args.port_notify))
elif self.args.mode == "embedded":
# TODO
pass
else:
raise NotImplementedError
def unsubscribe(self):
if self.args.mode == "standalone":
self.loop.run_until_complete(self.subscriber.close())
elif self.args.mode == "embedded":
# nothing to do
pass
else:
raise NotImplementedError
def run(self): def run(self):
self.args_init() self.args_init()
self.quamash_init() self.quamash_init()
try: try:
self.create_main_widget() self.ipc_init()
self.create_subscriber()
try: try:
self.loop.run_forever() self.create_main_widget()
self.subscribe()
try:
self.loop.run_forever()
finally:
self.unsubscribe()
finally: finally:
self.loop.run_until_complete(self.subscriber.close()) self.ipc_close()
finally: finally:
self.loop.close() self.loop.close()

View File

@ -12,7 +12,7 @@ from pyqtgraph import dockarea
from artiq import __artiq_dir__ as artiq_dir from artiq import __artiq_dir__ as artiq_dir
from artiq.tools import * from artiq.tools import *
from artiq.protocols.pc_rpc import AsyncioClient, Server from artiq.protocols.pc_rpc import AsyncioClient
from artiq.gui.models import ModelSubscriber from artiq.gui.models import ModelSubscriber
from artiq.gui import (state, experiments, shortcuts, explorer, from artiq.gui import (state, experiments, shortcuts, explorer,
moninj, datasets, applets, schedule, log, console) moninj, datasets, applets, schedule, log, console)
@ -111,9 +111,9 @@ def main():
d_datasets = datasets.DatasetsDock(win, dock_area, sub_clients["datasets"]) d_datasets = datasets.DatasetsDock(win, dock_area, sub_clients["datasets"])
appletmgr = applets.AppletManager(dock_area) d_applets = applets.AppletsDock(dock_area)
atexit_register_coroutine(appletmgr.stop) atexit_register_coroutine(d_applets.stop)
smgr.register(appletmgr) smgr.register(d_applets)
if os.name != "nt": if os.name != "nt":
d_ttl_dds = moninj.MonInj() d_ttl_dds = moninj.MonInj()
@ -133,11 +133,11 @@ def main():
if os.name != "nt": if os.name != "nt":
dock_area.addDock(d_ttl_dds.dds_dock, "top") dock_area.addDock(d_ttl_dds.dds_dock, "top")
dock_area.addDock(d_ttl_dds.ttl_dock, "above", d_ttl_dds.dds_dock) dock_area.addDock(d_ttl_dds.ttl_dock, "above", d_ttl_dds.dds_dock)
dock_area.addDock(appletmgr.main_dock, "above", d_ttl_dds.ttl_dock) dock_area.addDock(d_applets, "above", d_ttl_dds.ttl_dock)
dock_area.addDock(d_datasets, "above", appletmgr.main_dock) dock_area.addDock(d_datasets, "above", d_applets)
else: else:
dock_area.addDock(appletmgr.main_dock, "top") dock_area.addDock(d_applets, "top")
dock_area.addDock(d_datasets, "above", appletmgr.main_dock) dock_area.addDock(d_datasets, "above", d_applets)
dock_area.addDock(d_shortcuts, "above", d_datasets) dock_area.addDock(d_shortcuts, "above", d_datasets)
dock_area.addDock(d_explorer, "above", d_shortcuts) dock_area.addDock(d_explorer, "above", d_shortcuts)
dock_area.addDock(d_console, "bottom") dock_area.addDock(d_console, "bottom")
@ -153,11 +153,6 @@ def main():
if d_log0 is not None: if d_log0 is not None:
dock_area.addDock(d_log0, "right", d_explorer) dock_area.addDock(d_log0, "right", d_explorer)
# start RPC server
rpc_server = Server({"applets": appletmgr.rpc})
loop.run_until_complete(rpc_server.start("::1", 6501))
atexit_register_coroutine(rpc_server.stop)
# run # run
win.show() win.show()
loop.run_until_complete(win.exit_request.wait()) loop.run_until_complete(win.exit_request.wait())

View File

@ -7,17 +7,47 @@ from functools import partial
from quamash import QtCore, QtGui, QtWidgets from quamash import QtCore, QtGui, QtWidgets
from pyqtgraph import dockarea from pyqtgraph import dockarea
from artiq.protocols import pyon
from artiq.protocols.pipe_ipc import AsyncioParentComm
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class AppletIPCServer(AsyncioParentComm):
def __init__(self, capture_cb):
AsyncioParentComm.__init__(self)
self.capture_cb = capture_cb
def write_pyon(self, obj):
self.write(pyon.encode(obj).encode() + b"\n")
async def read_pyon(self):
line = await self.readline()
return pyon.decode(line.decode())
async def serve(self):
while True:
obj = await self.read_pyon()
try:
action = obj["action"]
if action == "embed":
self.capture_cb(obj["win_id"])
self.write_pyon({"action": "embed_done"})
else:
raise ValueError("unknown action in applet request")
except:
logger.warning("error processing applet request",
exc_info=True)
self.write_pyon({"action": "error"})
class AppletDock(dockarea.Dock): class AppletDock(dockarea.Dock):
def __init__(self, token, name, command): def __init__(self, name, command):
dockarea.Dock.__init__(self, "applet" + str(token), dockarea.Dock.__init__(self, "applet" + str(id(self)), # XXX
label="Applet: " + name, label="Applet: " + name,
closable=True) closable=True)
self.setMinimumSize(QtCore.QSize(500, 400)) self.setMinimumSize(QtCore.QSize(500, 400))
self.token = token
self.applet_name = name self.applet_name = name
self.command = command self.command = command
@ -26,41 +56,38 @@ class AppletDock(dockarea.Dock):
self.label.setText("Applet: " + name) self.label.setText("Applet: " + name)
async def start(self): async def start(self):
self.ipc = AppletIPCServer(self.capture)
command = self.command.format(python=sys.executable, command = self.command.format(python=sys.executable,
embed_token=self.token) ipc_address=self.ipc.get_address())
logger.debug("starting command %s for %s", command, self.applet_name) logger.debug("starting command %s for %s", command, self.applet_name)
try: try:
self.process = await asyncio.create_subprocess_exec( await self.ipc.create_subprocess(*shlex.split(command))
*shlex.split(command))
except: except:
logger.warning("Applet %s failed to start", self.applet_name, logger.warning("Applet %s failed to start", self.applet_name,
exc_info=True) exc_info=True)
asyncio.ensure_future(self.ipc.serve())
def capture(self, win_id): def capture(self, win_id):
logger.debug("capturing window 0x%x for %s", win_id, self.applet_name) logger.debug("capturing window 0x%x for %s", win_id, self.applet_name)
self.captured_window = QtGui.QWindow.fromWinId(win_id) captured_window = QtGui.QWindow.fromWinId(win_id)
self.captured_widget = QtWidgets.QWidget.createWindowContainer( captured_widget = QtWidgets.QWidget.createWindowContainer(
self.captured_window) captured_window)
self.addWidget(self.captured_widget) self.addWidget(captured_widget)
async def terminate(self): async def terminate(self):
if hasattr(self, "captured_window"):
self.captured_window.close()
self.captured_widget.deleteLater()
del self.captured_window
del self.captured_widget
if hasattr(self, "process"): if hasattr(self, "process"):
# TODO: send IPC termination request
try: try:
await asyncio.wait_for(self.process.wait(), 2.0) await asyncio.wait_for(self.ipc.process.wait(), 2.0)
except: except:
logger.warning("Applet %s failed to exit, killing", logger.warning("Applet %s failed to exit, killing",
self.applet_name) self.applet_name)
try: try:
self.process.kill() self.ipc.process.kill()
except ProcessLookupError: except ProcessLookupError:
pass pass
await self.process.wait() await self.ipc.process.wait()
del self.process del self.ipc
async def restart(self): async def restart(self):
await self.terminate() await self.terminate()
@ -69,24 +96,25 @@ class AppletDock(dockarea.Dock):
_templates = [ _templates = [
("Big number", "{python} -m artiq.applets.big_number " ("Big number", "{python} -m artiq.applets.big_number "
"--embed {embed_token} NUMBER_DATASET"), "embedded {ipc_address} NUMBER_DATASET"),
("Histogram", "{python} -m artiq.applets.plot_hist " ("Histogram", "{python} -m artiq.applets.plot_hist "
"--embed {embed_token} COUNTS_DATASET " "embedded {ipc_address} COUNTS_DATASET "
"--x BIN_BOUNDARIES_DATASET"), "--x BIN_BOUNDARIES_DATASET"),
("XY", "{python} -m artiq.applets.plot_xy " ("XY", "{python} -m artiq.applets.plot_xy "
"--embed {embed_token} Y_DATASET --x X_DATASET " "embedded {ipc_address} Y_DATASET --x X_DATASET "
"--error ERROR_DATASET --fit FIT_DATASET"), "--error ERROR_DATASET --fit FIT_DATASET"),
("XY + Histogram", "{python} -m artiq.applets.plot_xy_hist " ("XY + Histogram", "{python} -m artiq.applets.plot_xy_hist "
"--embed {embed_token} X_DATASET " "embedded {ipc_address} X_DATASET "
"HIST_BIN_BOUNDARIES_DATASET " "HIST_BIN_BOUNDARIES_DATASET "
"HISTS_COUNTS_DATASET"), "HISTS_COUNTS_DATASET"),
] ]
class AppletsDock(dockarea.Dock): class AppletsDock(dockarea.Dock):
def __init__(self, manager): def __init__(self, dock_area):
self.manager = manager self.dock_area = dock_area
self.token_to_checkbox = dict() self.dock_to_checkbox = dict()
self.workaround_pyqtgraph_bug = False
dockarea.Dock.__init__(self, "Applets") dockarea.Dock.__init__(self, "Applets")
self.setMinimumSize(QtCore.QSize(850, 450)) self.setMinimumSize(QtCore.QSize(850, 450))
@ -129,6 +157,18 @@ class AppletsDock(dockarea.Dock):
self.table.cellChanged.connect(self.cell_changed) self.table.cellChanged.connect(self.cell_changed)
def create(self, name, command):
dock = AppletDock(name, command)
# If a dock is floated and then dock state is restored, pyqtgraph
# leaves a "phantom" window open.
if self.workaround_pyqtgraph_bug:
self.dock_area.addDock(dock)
else:
self.dock_area.floatDock(dock)
asyncio.ensure_future(dock.start())
dock.sigClosed.connect(partial(self.on_dock_closed, dock))
return dock
def cell_changed(self, row, column): def cell_changed(self, row, column):
if column == 0: if column == 0:
item = self.table.item(row, column) item = self.table.item(row, column)
@ -141,27 +181,28 @@ class AppletsDock(dockarea.Dock):
name = "" name = ""
else: else:
name = name.text() name = name.text()
token = self.manager.create(name, command) dock = self.create(name, command)
item.applet_token = token item.applet_dock = dock
self.token_to_checkbox[token] = item self.dock_to_checkbox[dock] = item
else: else:
token = getattr(item, "applet_token", None) dock = getattr(item, "applet_dock", None)
if token is not None: if dock is not None:
# cell_changed is emitted at row creation # This calls self.on_dock_closed
self.manager.delete(token) dock.close()
elif column == 1 or column == 2: elif column == 1 or column == 2:
new_value = self.table.item(row, column).text() new_value = self.table.item(row, column).text()
token = getattr(self.table.item(row, 0), "applet_token", None) dock = getattr(self.table.item(row, 0), "applet_dock", None)
if token is not None: if dock is not None:
if column == 1: if column == 1:
self.manager.rename(token, new_value) dock.rename(new_value)
else: else:
self.manager.set_command(token, new_value) dock.command = new_value
def disable_token(self, token): def on_dock_closed(self, dock):
checkbox_item = self.token_to_checkbox[token] asyncio.ensure_future(dock.terminate())
checkbox_item.applet_token = None checkbox_item = self.dock_to_checkbox[dock]
del self.token_to_checkbox[token] checkbox_item.applet_dock = None
del self.dock_to_checkbox[dock]
checkbox_item.setCheckState(QtCore.Qt.Unchecked) checkbox_item.setCheckState(QtCore.Qt.Unchecked)
def new(self): def new(self):
@ -185,19 +226,26 @@ class AppletsDock(dockarea.Dock):
selection = self.table.selectedRanges() selection = self.table.selectedRanges()
if selection: if selection:
row = selection[0].topRow() row = selection[0].topRow()
token = getattr(self.table.item(row, 0), "applet_token", None) dock = getattr(self.table.item(row, 0), "applet_dock", None)
if token is not None: if dock is not None:
asyncio.ensure_future(self.manager.restart(token)) asyncio.ensure_future(dock.restart())
def delete(self): def delete(self):
selection = self.table.selectedRanges() selection = self.table.selectedRanges()
if selection: if selection:
row = selection[0].topRow() row = selection[0].topRow()
token = getattr(self.table.item(row, 0), "applet_token", None) dock = getattr(self.table.item(row, 0), "applet_dock", None)
if token is not None: if dock is not None:
self.manager.delete(token) # This calls self.on_dock_closed
dock.close()
self.table.removeRow(row) self.table.removeRow(row)
async def stop(self):
for row in range(self.table.rowCount()):
dock = getattr(self.table.item(row, 0), "applet_dock", None)
if dock is not None:
await dock.terminate()
def save_state(self): def save_state(self):
state = [] state = []
for row in range(self.table.rowCount()): for row in range(self.table.rowCount()):
@ -208,6 +256,7 @@ class AppletsDock(dockarea.Dock):
return state return state
def restore_state(self, state): def restore_state(self, state):
self.workaround_pyqtgraph_bug = True
for enabled, name, command in state: for enabled, name, command in state:
row = self.new() row = self.new()
item = QtWidgets.QTableWidgetItem() item = QtWidgets.QTableWidgetItem()
@ -218,72 +267,4 @@ class AppletsDock(dockarea.Dock):
self.table.setItem(row, 2, item) self.table.setItem(row, 2, item)
if enabled: if enabled:
self.table.item(row, 0).setCheckState(QtCore.Qt.Checked) self.table.item(row, 0).setCheckState(QtCore.Qt.Checked)
class AppletManagerRPC:
def __init__(self, parent):
self.parent = parent
def embed(self, token, win_id):
self.parent.embed(token, win_id)
class AppletManager:
def __init__(self, dock_area):
self.dock_area = dock_area
self.main_dock = AppletsDock(self)
self.rpc = AppletManagerRPC(self)
self.applet_docks = dict()
self.workaround_pyqtgraph_bug = False
def embed(self, token, win_id):
if token not in self.applet_docks:
logger.warning("Ignored incorrect embed token %d for winid 0x%x",
token, win_id)
return
self.applet_docks[token].capture(win_id)
def create(self, name, command):
token = next(iter(set(range(len(self.applet_docks) + 1))
- self.applet_docks.keys()))
dock = AppletDock(token, name, command)
self.applet_docks[token] = dock
# If a dock is floated and then dock state is restored, pyqtgraph
# leaves a "phantom" window open.
if self.workaround_pyqtgraph_bug:
self.dock_area.addDock(dock)
else:
self.dock_area.floatDock(dock)
asyncio.ensure_future(dock.start())
dock.sigClosed.connect(partial(self.on_dock_closed, token))
return token
def on_dock_closed(self, token):
asyncio.ensure_future(self.applet_docks[token].terminate())
self.main_dock.disable_token(token)
del self.applet_docks[token]
def delete(self, token):
# This in turns calls on_dock_closed and main_dock.disable_token
self.applet_docks[token].close()
def rename(self, token, name):
self.applet_docks[token].rename(name)
def set_command(self, token, command):
self.applet_docks[token].command = command
async def restart(self, token):
await self.applet_docks[token].restart()
async def stop(self):
for dock in self.applet_docks.values():
await dock.terminate()
def save_state(self):
return self.main_dock.save_state()
def restore_state(self, state):
self.workaround_pyqtgraph_bug = True
self.main_dock.restore_state(state)
self.workaround_pyqtgraph_bug = False self.workaround_pyqtgraph_bug = False