diff --git a/engine/api.py b/engine/api.py index d30e42b..c801562 100644 --- a/engine/api.py +++ b/engine/api.py @@ -178,6 +178,7 @@ def startEngine(): #Until we return from startEngine a GUI will also not create its mainwindow. This can be used to create the appearance #of a session-on-load: if PATHS["startupSession"]: + logger.info(f"Got start-session as command line parameter. Opening: {PATHS['startupSession']}") sessionOpen(PATHS["startupSession"]) #Info @@ -234,7 +235,7 @@ def sessionCopy(nsmSessionName:str, newName:str): nsmServerControl.copySession(nsmSessionName, newName) def sessionOpen(nsmSessionName:str): - """Saves the current session and loads a different existing session.""" + """Saves the current session and loads a different existing session.""" nsmServerControl.open(nsmSessionName) def sessionQuery(nsmSessionName:str): diff --git a/engine/nsmservercontrol.py b/engine/nsmservercontrol.py index 6fcc15d..61af764 100644 --- a/engine/nsmservercontrol.py +++ b/engine/nsmservercontrol.py @@ -395,8 +395,7 @@ class NsmServerControl(object): logger.info("nsmd is ready @ {}".format(self.nsmOSCUrl)) #Tell nsmd that we are a GUI and want to receive general messages async, not only after we request something - self.gui_announce() #Triggers "hi" and session root - self.sessionRoot = self._waitForSessionRootBlocking() + self.sessionRoot = self._initial_announce() #Triggers "hi" and session root self.internalState["sessionRoot"] = self.sessionRoot atexit.register(self.quit) #mostly does stuff when we started nsmd ourself @@ -430,7 +429,6 @@ class NsmServerControl(object): #print ("not executed") return False - def processSingleInstance(self): """Tests our unix socket for an incoming signal. if received forward to the engine->gui @@ -524,67 +522,72 @@ class NsmServerControl(object): self.ourOwnServer = subprocess.Popen(["nsmd","--osc-port", str(port)]) + def _blockingRequest(self, path:str, arguments:list, answerPath:str, answerArguments:list, repeat=False)->list: + """During start-up we need to wait for replies. Also some operations only make sense + if we got data back. This is an abstraction that deals with messages that may come + out-of-order and keeps them for later, but at least prevents our side from sending + messages out-of-order itself. - #Category: Better safe than sorry - def _waitForPingResponseBlocking(self): - """Only used to test if the nsm server is ready. - Uses timeout as waiting time. + Default is: send once, wait for answer. repeat=True sends multiple times until an answer arrives. - This cannot be used after our thread with run_receivingServer has started because - the ping reply will be received by this thread instead. + Returns list of arguments, can be empty. """ - self._setPause(True) - self.sock.settimeout(0.01) - - logger.info("Sending /osc/ping") - out_msg = _OutgoingMessage("/osc/ping") - - while True: - self.sock.sendto(out_msg.build(), self.nsmOSCUrl) #we need to send multiple times. If the server is not ready it can't receive the ping :) - try: - data, addr = self.sock.recvfrom(1024) - msg = _IncomingMessage(data) - break - except BlockingIOError: #happens while no data is received. Has nothing to do with blocking or not. - continue - except socket.timeout: - continue - - if msg.oscpath == "/reply" and msg.params[0] == "/osc/ping": - logger.info("Got ping response") - return True - else: - logger.error(f"Waiting for ping, but got path: {msg.oscpath} with {msg.params}. Adding to queue for later. If the server got started anyway and is reacting to your commands, it is fine for now.") - self._queue.append(msg) + assert not self._queue, [(m.oscpath, m.params) for m in self._queue] + + logger.info(f"[wait for answer]: Sending {path}: {arguments}") + self._setPause(True) + out_msg = _OutgoingMessage(path) + for arg in arguments: + out_msg.add_arg(arg) - self._setPause(False) + if not repeat: + self.sock.sendto(out_msg.build(), self.nsmOSCUrl) - def _waitForSessionRootBlocking(self): - """Arrives after GUI Announce 'hi' - There is only one session root """ - logger.info("Waiting for session root message in blocking mode") - self._setPause(True) + #Wait for answer ready = False while not ready: + if repeat: #we need to send multiple times. + self.sock.sendto(out_msg.build(), self.nsmOSCUrl) try: data, addr = self.sock.recvfrom(1024) msg = _IncomingMessage(data) - - if msg.oscpath == "/nsm/gui/session/root": - sessionRoot = msg.params[0] + if answerArguments and msg.oscpath == answerPath and msg.params == answerArguments: + result = msg.params + logger.info(f"[wait from {path}] Received {answerPath}: {result}") + ready = True + elif msg.oscpath == answerPath: + result = msg.params + logger.info(f"[wait from {path}] Received {answerPath}: {result}") ready = True else: - logger.error(f"Waiting for session root from nsmd, but got: {msg.oscpath} with {msg.params}. Adding to queue for later. If the server got started anyway and is reacting to your commands, it is fine for now.") + logger.warning(f"Waiting for {answerPath} from nsmd, but got: {msg.oscpath} with {msg.params}. Adding to queue for later.") self._queue.append(msg) - except BlockingIOError: #happens while no data is received. Has nothing to do with blocking or not. continue except socket.timeout: continue - logger.info(f"Session root directory is {sessionRoot}") self._setPause(False) - return sessionRoot + return result + + + def _waitForPingResponseBlocking(self): + self._blockingRequest(path="/osc/ping", arguments=[], answerPath="/reply", answerArguments=["/osc/ping",], repeat=True) + + def _initial_announce(self)->pathlib.Path: + """nsm/gui/gui_announce triggers a multi-stage reply. First we get "hi", + then we get the session root. We wait for session root and then clean 'hi' from the queue. + + Returns session root as pathlib-path.""" + resultArguments = self._blockingRequest(path="/nsm/gui/gui_announce", arguments=[], answerPath="/nsm/gui/session/root", answerArguments=[]) + if len(self._queue) == 1 and self._queue[0].oscpath == "/nsm/gui/gui_announce" and self._queue[0].params == ["hi"]: + self._queue.clear() + else: + logging.error("For ValueError below:", [(m.oscpath, m.params) for m in self._queue]) + raise ValueError("We were expecting a clean _queue with only 'hi' as leftover, but instead there were unhandled messages. see print above. Better abort than a wrong program state") + + #all ok + return pathlib.Path(resultArguments[0]) #General Commands def send(self, arg): @@ -599,6 +602,8 @@ class NsmServerControl(object): self.sock.sendto(msg.build(), self.nsmOSCUrl) def gui_announce(self): + """This is just the announce without any answer. This is a last-resort method if another GUI + "stole" our slot. For our own initial announce we use self._initial_announce()""" msg = _OutgoingMessage("/nsm/gui/gui_announce") self.sock.sendto(msg.build(), self.nsmOSCUrl) @@ -619,7 +624,7 @@ class NsmServerControl(object): First is /nsm/gui/server/message ['Listing sessions'] Then session names come one reply at a time such as /reply ['/nsm/server/list', 'test3'] - Finally /nsm/server/list [0, 'Done.'] , not a reply + Finally /nsm/server/list [0, 'Done.'] , not a reply """ logger.info("Requesting project list from session server in blocking mode") self._setPause(True) @@ -643,6 +648,7 @@ class NsmServerControl(object): logger.warning(f"Expected project but got path {msg.oscpath} with {msg.params}. Adding to queue for later.") self._queue.append(msg) continue + #This is what we want: elif msg.oscpath == "/reply" and msg.params[0] == "/nsm/server/list": #/reply ['/nsm/server/list', 'test3'] self.internalState["sessions"].add(msg.params[1]) @@ -683,7 +689,7 @@ class NsmServerControl(object): self.sock.sendto(message.build(), self.nsmOSCUrl) #Primarily Without Session - def open(self, nsmSessionName:str): + def open(self, nsmSessionName:str): if nsmSessionName in self.internalState["sessions"]: msg = _OutgoingMessage("/nsm/server/open") msg.add_arg(nsmSessionName) #s:project_name @@ -1405,7 +1411,7 @@ class NsmServerControl(object): entry["sessionFile"] = sessionFile entry["lockFile"] = pathlib.Path(basePath, ".lock") entry["fullPath"] = str(basePath) - entry["sizeInBytes"] = sum(f.stat().st_size for f in basePath.glob('**/*') if f.is_file() ) + entry["sizeInBytes"] = sum(f.stat().st_size for f in basePath.glob('**/*') if f.is_file() ) entry["numberOfClients"] = len(open(sessionFile).readlines()) entry["hasSymlinks"] = self._checkDirectoryForSymlinks(basePath) entry["parents"] = basePath.relative_to(self.sessionRoot).parts[:-1] #tuple of each dir between NSM root and nsmSessionName/session.nsm, exluding the actual project name. This is the tree diff --git a/qtgui/mainwindow.py b/qtgui/mainwindow.py index 432a18a..9acc2d3 100644 --- a/qtgui/mainwindow.py +++ b/qtgui/mainwindow.py @@ -116,7 +116,7 @@ class MainWindow(QtWidgets.QMainWindow): #TODO: Hide information tab until the feature is ready self.ui.tabbyCat.removeTab(2) - + self.sessionController = SessionController(mainWindow=self) self.systemTray = SystemTray(mainWindow=self) self.connectMenu() @@ -138,7 +138,6 @@ class MainWindow(QtWidgets.QMainWindow): logger.info("Show MainWindow") self.restoreWindowSettings() #includes show/hide - #Handle the application data cache. If not present instruct the engine to build one. #This is also needed by the prompt in sessionController settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) @@ -158,6 +157,24 @@ class MainWindow(QtWidgets.QMainWindow): qtApp.exec_() #No code after exec_ + + def tabtest(self): + import subprocess + from time import sleep + #xdotool search --name xeyes + #xdotool search --pid 12345 + subprocess.Popen(["patchage"], shell=True, stdin=None, stdout=None, stderr=None, close_fds=True) #parameters are for not waiting + sleep(1) + result = subprocess.run(["xdotool", "search", "--name", "patchage"], stdout=subprocess.PIPE).stdout.decode('utf-8') + if "\n" in result: + windowID = int(result.split("\n")[0]) + else: + windowID = int(result) + window = QtGui.QWindow.fromWinId(int(windowID)) + window.setFlags(QtCore.Qt.FramelessWindowHint) + widget = QtWidgets.QWidget.createWindowContainer(window) + self.ui.tabbyCat.addTab(widget, "Patchage") + def hideEvent(self, event): if self.systemTray.available: super().hideEvent(event) diff --git a/qtgui/quickopensessioncontroller.py b/qtgui/quickopensessioncontroller.py index 2cd4689..ceed267 100644 --- a/qtgui/quickopensessioncontroller.py +++ b/qtgui/quickopensessioncontroller.py @@ -275,7 +275,8 @@ class QuickOpenSessionController(object): for entry in whitelist: exe = entry["argodejoExec"] if exe in StarterClientItem.allItems: - leftovers.remove(entry["argodejoExec"]) + if entry["argodejoExec"] in leftovers: #It happened that it was not. Don't ask me... + leftovers.remove(entry["argodejoExec"]) else: #Create new. Item will be parented by Qt, so Python GC will not delete item = StarterClientItem(parentController=self, desktopEntry=entry) diff --git a/qtgui/quicksessioncontroller.py b/qtgui/quicksessioncontroller.py index 02d715b..1034c52 100644 --- a/qtgui/quicksessioncontroller.py +++ b/qtgui/quicksessioncontroller.py @@ -47,9 +47,8 @@ class SessionButton(QtWidgets.QPushButton): self.setFont(font) def openSession(self): - name = self.sessionDict["nsmSessionName"] - api.sessionOpen(name) - + name = self.sessionDict["nsmSessionName"] + api.sessionOpen(name) class QuickSessionController(object): """Controls the widget, but does not subclass"""