diff --git a/artiq/dashboard/applets_ccb.py b/artiq/dashboard/applets_ccb.py index 7fd984cae..26f364949 100644 --- a/artiq/dashboard/applets_ccb.py +++ b/artiq/dashboard/applets_ccb.py @@ -53,10 +53,14 @@ class AppletsCCBDock(applets.AppletsDock): if not self.listen_action.isChecked(): return parent, applet = self.locate_applet(name, group, True) - if applet is None: - applet = self.new(name=name, command=command_or_code, parent=parent) + if is_code: + spec = {"ty": "code", "code": command_or_code} else: - applet.setText(2, command_or_code) + spec = {"ty": "command", "command": command_or_code} + if applet is None: + applet = self.new(name=name, spec=spec, parent=parent) + else: + self.set_spec(applet, spec) applet.setCheckState(0, QtCore.Qt.Checked) def ccb_disable_applet(self, name, group=None): diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index 6874cdcfc..d8fd2e422 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -91,7 +91,7 @@ class AppletIPCServer(AsyncioParentComm): class _AppletDock(QDockWidgetCloseDetect): - def __init__(self, datasets_sub, uid, name, command): + def __init__(self, datasets_sub, uid, name, spec): QDockWidgetCloseDetect.__init__(self, "Applet: " + name) self.setObjectName("applet" + str(uid)) @@ -101,7 +101,7 @@ class _AppletDock(QDockWidgetCloseDetect): self.datasets_sub = datasets_sub self.applet_name = name - self.command = command + self.spec = spec self.starting_stopping = False @@ -112,30 +112,27 @@ class _AppletDock(QDockWidgetCloseDetect): def _get_log_source(self): return "applet({})".format(self.applet_name) - async def start(self): + async def start_process(self, args, stdin): if self.starting_stopping: return self.starting_stopping = True try: self.ipc = AppletIPCServer(self.datasets_sub) - command_tpl = string.Template(self.command) - python = sys.executable.replace("\\", "\\\\") - command = command_tpl.safe_substitute( - python=python, - artiq_applet=python + " -m artiq.applets." - ) - logger.debug("starting command %s for %s", command, self.applet_name) env = os.environ.copy() env["PYTHONUNBUFFERED"] = "1" env["ARTIQ_APPLET_EMBED"] = self.ipc.get_address() try: await self.ipc.create_subprocess( - *shlex.split(command), + *args, + stdin=None if stdin is None else subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, start_new_session=True) except: logger.warning("Applet %s failed to start", self.applet_name, exc_info=True) + if stdin is not None: + self.ipc.process.stdin.write(stdin.encode()) + self.ipc.process.stdin.write_eof() asyncio.ensure_future( LogParser(self._get_log_source).stream_task( self.ipc.process.stdout)) @@ -146,6 +143,21 @@ class _AppletDock(QDockWidgetCloseDetect): finally: self.starting_stopping = False + async def start(self): + if self.spec["ty"] == "command": + command_tpl = string.Template(self.spec["command"]) + python = sys.executable.replace("\\", "\\\\") + command = command_tpl.safe_substitute( + python=python, + artiq_applet=python + " -m artiq.applets." + ) + logger.debug("starting command %s for %s", command, self.applet_name) + await self.start_process(shlex.split(command), None) + elif self.spec["ty"] == "code": + await self.start_process([sys.executable], self.spec["code"]) + else: + raise ValueError + def embed(self, win_id): logger.debug("capturing window 0x%x for %s", win_id, self.applet_name) self.embed_window = QtGui.QWindow.fromWinId(win_id) @@ -334,9 +346,10 @@ class AppletsDock(QtWidgets.QDockWidget): self.table.addAction(new_action) templates_menu = QtWidgets.QMenu() for name, template in _templates: + spec = {"ty": "command", "command": template} action = QtWidgets.QAction(name, self.table) action.triggered.connect(partial( - self.new_with_parent, self.new, command=template)) + self.new_with_parent, self.new, spec=spec)) templates_menu.addAction(action) restart_action = QtWidgets.QAction("New applet from template", self.table) restart_action.setMenu(templates_menu) @@ -357,8 +370,38 @@ class AppletsDock(QtWidgets.QDockWidget): self.table.itemChanged.connect(self.item_changed) - def create(self, uid, name, command): - dock = _AppletDock(self.datasets_sub, uid, name, command) + def get_spec(self, item): + if item.applet_spec_ty == "command": + return {"ty": "command", "command": item.text(2)} + elif item.applet_spec_ty == "code": + return {"ty": "code", "code": item.applet_code} + else: + raise ValueError + + def set_spec(self, item, spec): + self.table.itemChanged.disconnect() + try: + item.applet_spec_ty = spec["ty"] + if spec["ty"] == "command": + item.setText(2, spec["command"]) + item.setIcon(2, QtGui.QIcon()) + if hasattr(item, "applet_code"): + del item.applet_code + elif spec["ty"] == "code": + item.setText(2, "(code)") + item.setIcon(2, QtWidgets.QApplication.style().standardIcon( + QtWidgets.QStyle.SP_FileIcon)) + item.applet_code = spec["code"] + else: + raise ValueError + dock = item.applet_dock + if dock is not None: + dock.spec = spec + finally: + self.table.itemChanged.connect(self.item_changed) + + def create(self, uid, name, spec): + dock = _AppletDock(self.datasets_sub, uid, name, spec) self.main_window.addDockWidget(QtCore.Qt.RightDockWidgetArea, dock) dock.setFloating(True) asyncio.ensure_future(dock.start()) @@ -369,16 +412,15 @@ class AppletsDock(QtWidgets.QDockWidget): if item.ty == "applet": if column == 0: if item.checkState(0) == QtCore.Qt.Checked: - command = item.text(2) - if command: - name = item.text(1) - dock = self.create(item.applet_uid, name, command) - item.applet_dock = dock - if item.applet_geometry is not None: - dock.restoreGeometry(item.applet_geometry) - # geometry is now handled by main window state - item.applet_geometry = None - self.dock_to_item[dock] = item + name = item.text(1) + spec = self.get_spec(item) + dock = self.create(item.applet_uid, name, spec) + item.applet_dock = dock + if item.applet_geometry is not None: + dock.restoreGeometry(item.applet_geometry) + # geometry is now handled by main window state + item.applet_geometry = None + self.dock_to_item[dock] = item else: dock = item.applet_dock if dock is not None: @@ -391,7 +433,8 @@ class AppletsDock(QtWidgets.QDockWidget): if column == 1: dock.rename(new_value) else: - dock.command = new_value + self.set_spec( + item, {"ty": "command", "command": new_value}) elif item.ty == "group": # To Qt's credit, it already does everything for us here. pass @@ -422,15 +465,17 @@ class AppletsDock(QtWidgets.QDockWidget): name = "untitled " + str(i) return name - def new(self, uid=None, name=None, command="", parent=None): + def new(self, uid=None, name=None, spec=None, parent=None): if uid is None: uid = next(i for i in count() if i not in self.applet_uids) + if spec is None: + spec = {"ty": "command", "command": ""} assert uid not in self.applet_uids, uid self.applet_uids.add(uid) if name is None: name = self.get_untitled() - item = QtWidgets.QTreeWidgetItem(["", name, command]) + item = QtWidgets.QTreeWidgetItem(["", name, ""]) item.ty = "applet" item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsUserCheckable | @@ -444,6 +489,7 @@ class AppletsDock(QtWidgets.QDockWidget): item.applet_geometry = None item.setIcon(0, QtWidgets.QApplication.style().standardIcon( QtWidgets.QStyle.SP_ComputerIcon)) + self.set_spec(item, spec) if parent is None: self.table.addTopLevelItem(item) else: @@ -543,11 +589,11 @@ class AppletsDock(QtWidgets.QDockWidget): uid = cwi.applet_uid enabled = cwi.checkState(0) == QtCore.Qt.Checked name = cwi.text(1) - command = cwi.text(2) + spec = self.get_spec(cwi) geometry = cwi.applet_geometry if geometry is not None: geometry = bytes(geometry) - state.append(("applet", uid, enabled, name, command, geometry)) + state.append(("applet", uid, enabled, name, spec, geometry)) elif cwi.ty == "group": name = cwi.text(1) expanded = cwi.isExpanded() @@ -563,8 +609,11 @@ class AppletsDock(QtWidgets.QDockWidget): def restore_state_item(self, state, parent): for wis in state: if wis[0] == "applet": - _, uid, enabled, name, command, geometry = wis - item = self.new(uid, name, command, parent=parent) + _, uid, enabled, name, spec, geometry = wis + if spec["ty"] not in {"command", "code"}: + raise ValueError("Invalid applet spec type: " + + str(spec["ty"])) + item = self.new(uid, name, spec, parent=parent) if geometry is not None: geometry = QtCore.QByteArray(geometry) item.applet_geometry = geometry