Browse Source

Restructure blocking request during startup to hopefully prevent infinite loop

master
Nils 4 years ago
parent
commit
e994d16bee
  1. 3
      engine/api.py
  2. 102
      engine/nsmservercontrol.py
  3. 21
      qtgui/mainwindow.py
  4. 3
      qtgui/quickopensessioncontroller.py
  5. 5
      qtgui/quicksessioncontroller.py

3
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):

102
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

21
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)

3
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)

5
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"""

Loading…
Cancel
Save