diff --git a/CHANGELOG b/CHANGELOG index 427d071..f22b739 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,7 @@ External contributors notice at the end of the line: (LastName, FirstName / nick Initial startup-check for old $HOME session root vs new XDG root. Offers to move the session files to the new location. Support nsmd 1.6.0 lockfiles. Older nsmd versions still work. +On startup give a choice to connect to an already running session (nsmd 1.6.0) ## 2022-01-15 0.3.1 diff --git a/engine/config.py b/engine/config.py index 214eb2e..f0add17 100644 --- a/engine/config.py +++ b/engine/config.py @@ -21,7 +21,7 @@ METADATA={ #release announcements, entries in software directories etc. "tagline" : 'Music and audio production session manager based on NSM.', - "version" : "0.3.1", + "version" : "0.4.0", "year" : "2022", "author" : "Laborejo Software Suite", "url" : "https://www.laborejo.org/agordejo", diff --git a/engine/nsmservercontrol.py b/engine/nsmservercontrol.py index 918cac7..bb8775a 100644 --- a/engine/nsmservercontrol.py +++ b/engine/nsmservercontrol.py @@ -371,12 +371,15 @@ class NsmServerControl(object): #self.nsmOSCUrl must be a tuple compatible to the result of urlparse. (hostname, port) self.singleInstanceSocket = None if parameterNsmOSCUrl: + #There is either a user provided nsm url via --url commandline or paramter the GUI detected a running nsmd and let the use choose that. o = urlparse(parameterNsmOSCUrl) #self.nsmOSCUrl = (o.hostname, o.port) #this forces lowercase. in rare circumstances this is not correct and we must be case sensitive. fix: self.nsmOSCUrl = o.netloc.split(":")[0], o.port else: envResult = self._getNsmOSCUrlFromEnvironment() if envResult: + #In case there is no actual nsmd running but there still was a NSM_URL env var, e.g. over the network, use this. + #There is a corner case that the env is local but the user chose to ignore the GUI way (nsmd 1.6.0) to proivde us directly with a specific URL. self.nsmOSCUrl = envResult else: #This is the default case. User just starts the GUI. The other modes are concious decisions to either start with URL as parameter or in an NSM environment. diff --git a/engine/start.py b/engine/start.py index da8ed1c..97e5c11 100644 --- a/engine/start.py +++ b/engine/start.py @@ -127,7 +127,6 @@ else: "continueLastSession": args.continueLastSession, #bool } - if PATHS["startupSession"]: logger.warning("--continue ignored because --load-session was used.") PATHS["continueLastSession"] = None #just in case. See --help string diff --git a/qtgui/mainwindow.py b/qtgui/mainwindow.py index 3f72063..1277f3b 100644 --- a/qtgui/mainwindow.py +++ b/qtgui/mainwindow.py @@ -50,6 +50,7 @@ from .resources import * from .settings import SettingsDialog from .jacktransport import JackTransportControls from .movesessionroot import xdgVersionChange +from .startchooserunningnsmd import checkForRunningNsmd api.eventLoop = EventLoop() @@ -164,7 +165,8 @@ class MainWindow(QtWidgets.QMainWindow): self.ui.stack_loaded_session.customContextMenuRequested.connect(self.customContextMenu) #nsmd 1.6.0 - xdgVersionChange(self.qtApp) + xdgVersionChange(self.qtApp) #may present a blocking dialog, may do nothing. + checkForRunningNsmd(self.qtApp, PATHS) #may present a blocking dialog, may do nothing. Injects nsm url into PATHS #Api Callbacks api.callbacks.sessionClosed.append(self.reactCallback_sessionClosed) diff --git a/qtgui/movesessionroot.py b/qtgui/movesessionroot.py index 5aadcef..be32ff1 100644 --- a/qtgui/movesessionroot.py +++ b/qtgui/movesessionroot.py @@ -71,7 +71,7 @@ def xdgVersionChange(qtApp): except RuntimeError: #no home dir! oldexists = False - xdgdatahome = getenv("$XDG_DATA_HOME") + xdgdatahome = getenv("XDG_DATA_HOME") if not xdgdatahome: try: xdgdatahome = pathlib.Path("~/.local/share").expanduser() diff --git a/qtgui/startchooserunningnsmd.py b/qtgui/startchooserunningnsmd.py new file mode 100644 index 0000000..26f81ab --- /dev/null +++ b/qtgui/startchooserunningnsmd.py @@ -0,0 +1,160 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ). + +This application is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +import logging; logger = logging.getLogger(__name__); logger.info("import") + +#Standard Library +import pathlib +from os import getenv, listdir +from sys import argv as sysargv + + +#Third Party +from PyQt5 import QtCore, QtGui, QtWidgets + +#Our own files +from .movesessionroot import nsmVersionGreater160 + + +def checkForRunningNsmd(qtApp, PATHS:dict): + """Before we start the engine with our own nsmd server: + nsmd >= 1.6.0 introduced run file discovery of sessions and empty nsmd. + + Ask the user if they want to connect to one of these. + + Injects the path into PATHS["url"], which is used by engine.api.startEngine() + """ + + + if PATHS["url"] or any("url" in param for param in sysargv): #this is really the same test twice... + #This is not our problem anymore. + return + + + if not nsmVersionGreater160(): + return #see docstring + + parent = qtApp.desktop() + + + if not getenv("XDG_RUNTIME_DIR"): + logger.warning("Your system has no environment variable $XDG_RUNTIME_DIR. That is unusual for Linux. Automatic detection of already running sessions deactivated.") + return #On Linux that should exist. But if not, no reason to crash. + + + logger.info("Detecting if there are already nsmd or sessions running under this user") + + session_rundir = pathlib.Path(getenv("XDG_RUNTIME_DIR"), "nsm") + logger.info(f"Supposed nsmd session rundir: {session_rundir}") + nsmd_rundir = pathlib.Path(getenv("XDG_RUNTIME_DIR"), "nsm", "d") + logger.info(f"Supposed nsmd server rundir: {nsmd_rundir}") + + if not session_rundir.exists() or not session_rundir.is_dir(): + logger.info("nsmd rundir does not exist. Continue.") + return + + existing_daemons = listdir(nsmd_rundir) + existing_sessions = listdir(session_rundir) + if existing_sessions: + existing_sessions.remove("d") #Remove the daemon subdir + + if not existing_daemons: + logger.info("nsmd rundir exist, but is empty. Continue.") + return #if there are no daemons there are no sessions. + + #There are nsmd running under this user, maybe even open sessions. + #Now build a list for the user to choose from. + #If a session is running on an nsmd show this, otherwise show the empty nsmd + sessions = [] + nsmd_pids = set() + for s in existing_sessions: + res = {} + sessions.append(res) + with open(pathlib.Path(session_rundir, s), "r") as f: + res["hashName"] = s #name of the lockfile. has hash postfix. + res["path"] = pathlib.Path(f.readline().strip("\n")) #file path in ~/.local/share/nsm + res["name"] = res["path"].name #present this to the user. + res["url"] = f.readline().strip("\n") #nsmd url + res["pid"] = int(f.readline().strip("\n")) #nsmd pid + nsmd_pids.add(res["pid"]) + + + daemons = [] + for d in existing_daemons: + #d is a file with a PID as name. e.g. 9627 + #inside is the NSM_URL. + if int(d) in nsmd_pids: #we already have a running session on this server + continue + else: #empty nsmd. + res = {} + daemons.append(res) + with open(pathlib.Path(nsmd_rundir, d), "r") as f: + res["pid"] = d + res["url"] = f.readline().strip("\n") #just the first line + res["name"] = QtCore.QCoreApplication.translate("StartChooseRunningSession", "Empty Server") + " " + res["url"] + + #We now have all empty nsmd in var daemons + + assert sessions or daemons + + PATHS["url"] = ChooseSessionWidget(qtApp, sessions+daemons).url #"Global Variable" + + +class ChooseSessionWidget(QtWidgets.QDialog): + """return value in self.url. Might be None""" + + def __init__(self, qtApp, sessionDicts:list): + + super().__init__() #QDialog can't parent to qtApp + + self.qtApp = qtApp + self.setModal(True) #block until closed + + self.layout = QtWidgets.QVBoxLayout() + self.setLayout(self.layout) + + self.label = QtWidgets.QLabel(self) + self.label.setText(QtCore.QCoreApplication.translate("StartChooseRunningSession", "Select session or server to connect to.\nCancel to start our own server.")) + self.layout.addWidget(self.label) + + self.comboBox = QtWidgets.QComboBox(self) + self.layout.addWidget(self.comboBox) + + for s in sessionDicts: + self.comboBox.addItem(s["name"], s["url"]) #Show name, use url as data. + + self.buttonBox = QtWidgets.QDialogButtonBox(self) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.layout.addWidget(self.buttonBox) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + self.exec() + + def accept(self): + self.url = self.comboBox.currentData() #easy abstraction so that the caller does not need to know our widget name + super().accept() + + def reject(self): + self.url = None + super().reject()