Browse Source

Exchange generic logging with filespecific logger

master
Nils 5 years ago
parent
commit
9e0c9eb704
  1. 6
      template/engine/api.py
  2. 4
      template/engine/data.py
  3. 6
      template/engine/duration.py
  4. 4
      template/engine/history.py
  5. 4
      template/engine/input_midi.py
  6. 4
      template/engine/metronome.py
  7. 4
      template/engine/midi.py
  8. 12
      template/engine/pitch.py
  9. 14
      template/engine/sampler_sf2.py
  10. 35
      template/engine/sequencer.py
  11. 29
      template/engine/session.py
  12. 2
      template/qtgui/about.py
  13. 5
      template/qtgui/chooseSessionDirectory.py
  14. 3
      template/qtgui/constantsAndConfigs.py
  15. 5
      template/qtgui/debugScript.py
  16. 16
      template/qtgui/mainwindow.py
  17. 3
      template/qtgui/menu.py
  18. 183
      template/qtgui/nsmclient.py
  19. 5
      template/qtgui/nsmsingleserver.py
  20. 3
      template/qtgui/submenus.py
  21. 3
      template/qtgui/usermanual.py
  22. 38
      template/start.py

6
template/engine/api.py

@ -20,7 +20,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging; logging.info("import {}".format(__file__))
import logging; logger = logging.getLogger(__name__); logger.info("import")
#Python Standard Library
import os.path
@ -83,7 +85,7 @@ class Callbacks(object):
Insert, delete edit are real data changes. Cursor movement or playback ticks are not."""
session.nsmClient.announceSaveStatus(False)
self._historyChanged()
self._historyChanged()
def _historyChanged(self):

4
template/engine/data.py

@ -20,7 +20,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging; logging.info("import {}".format(__file__))
import logging; logger = logging.getLogger(__name__); logger.info("import")
class Data(object):
"""Base class to all Data. Data is the class that gets added to the session. Consider this an

6
template/engine/duration.py

@ -24,7 +24,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
This file handles various durations and their conversions.
"""
import logging; logging.info("import {}".format(__file__))
import logging; logger = logging.getLogger(__name__); logger.info("import")
from engine.config import METADATA
@ -47,7 +49,7 @@ D512 = int(D256 / 2)
D1024 = int(D512 / 2) # set this to a number with many factors, like 210. According to http://homes.sice.indiana.edu/donbyrd/CMNExtremes.htm this is the real world limit.
if not int(D1024) == D1024:
logging.error(f"Warning: Lowest duration D0124 has decimal places: {D0124} but should be a plain integer. Your D4 value: {D4} has not enough 2^n factors. ")
logger.error(f"Warning: Lowest duration D0124 has decimal places: {D0124} but should be a plain integer. Your D4 value: {D4} has not enough 2^n factors. ")
D2 = D4 *2
D1 = D2 *2

4
template/engine/history.py

@ -19,7 +19,9 @@ 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 <http://www.gnu.org/licenses/>.
"""
import logging; logging.info("import {}".format(__file__))
import logging; logger = logging.getLogger(__name__); logger.info("import")
from contextlib import contextmanager

4
template/engine/input_midi.py

@ -19,7 +19,9 @@ 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 <http://www.gnu.org/licenses/>.
"""
import logging; logging.info("import {}".format(__file__))
import logging; logger = logging.getLogger(__name__); logger.info("import")
#Python Standard Library

4
template/engine/metronome.py

@ -20,7 +20,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging; logging.info("import {}".format(__file__))
import logging; logger = logging.getLogger(__name__); logger.info("import")
#Standard Library Modules
from typing import Tuple

4
template/engine/midi.py

@ -24,7 +24,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
This file handles various pitches and their conversions.
"""
import logging; logging.info("import {}".format(__file__))
import logging; logger = logging.getLogger(__name__); logger.info("import")
#Third Party Modules
#Template Modules

12
template/engine/pitch.py

@ -24,7 +24,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
This file handles various pitches and their conversions.
"""
import logging; logging.info("import {}".format(__file__))
import logging; logger = logging.getLogger(__name__); logger.info("import")
#Third Party Modules
#Template Modules
@ -328,10 +330,10 @@ pillarOfFifth = [
def midiPitchLimiter(pitch, transpose):
if pitch + transpose < 0:
logging.warning(f"Tranpose lead to a note below midi value 0: {pitch}. Limiting to 0. Please fix manually")
logger.warning(f"Tranpose lead to a note below midi value 0: {pitch}. Limiting to 0. Please fix manually")
return 0
elif pitch + transpose > 127:
logging.warning(f"Tranpose lead to a note above midi value 127: {pitch}. Limiting to 127. Please fix manually")
logger.warning(f"Tranpose lead to a note above midi value 127: {pitch}. Limiting to 127. Please fix manually")
return 127
else:
return pitch + transpose
@ -339,10 +341,10 @@ def midiPitchLimiter(pitch, transpose):
def midiChannelLimiter(value):
"""makes sure that a midi channel is in range 0-15"""
if value > 15:
logging.warning("Midi Channel bigger 15 detected: {}. Limiting to 15. Please fix manually".format(value))
logger.warning("Midi Channel bigger 15 detected: {}. Limiting to 15. Please fix manually".format(value))
return 15
elif value <0:
logging.warning("Midi Channel smaller 0 detected: {}. Limiting to 0. Please fix manually".format(value))
logger.warning("Midi Channel smaller 0 detected: {}. Limiting to 0. Please fix manually".format(value))
return 0
else:
return value

14
template/engine/sampler_sf2.py

@ -20,7 +20,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging; logging.info("import {}".format(__file__))
import logging; logger = logging.getLogger(__name__); logger.info("import")
#Python Standard Lib
import os.path
@ -89,7 +91,7 @@ class Sampler_sf2(Data):
try:
cbox.JackIO.Metadata.set_port_order(portname, channelNumber)
except Exception as e: #No Jack Meta Data
logging.error(e)
logger.error(e)
#Also sort the mixing channels
try:
@ -98,7 +100,7 @@ class Sampler_sf2(Data):
portname = f"{cbox.JackIO.status().client_name}:right_mix"
cbox.JackIO.Metadata.set_port_order(portname, 34)
except Exception as e: #No Jack Meta Data
logging.error(e)
logger.error(e)
@ -139,7 +141,7 @@ class Sampler_sf2(Data):
def loadSoundfont(self, filePath, defaultSoundfont=None):
"""defaultSoundfont is a special case. The path is not saved"""
logging.info(f"loading path: \"{filePath}\" defaultSoundfont parameter: {defaultSoundfont}")
logger.info(f"loading path: \"{filePath}\" defaultSoundfont parameter: {defaultSoundfont}")
#Remove the old link, if present. We cannot unlink directly in loadSoundfont because it is quite possible that a user will try out another soundfont but decide not to save but close and reopen to get his old soundfont back.
if self.filePath and os.path.islink(self.filePath):
@ -188,7 +190,7 @@ class Sampler_sf2(Data):
for youchoose_bank in self.patchlist.keys():
for youchoose_program, youchoose_name in self.patchlist[youchoose_bank].items():
youchoose_bank, youchoose_program, youchoose_name
logging.info(METADATA["name"] + f": Channel {channel} Old bank/program not possible in new soundfont. Switching to first available program.")
logger.info(METADATA["name"] + f": Channel {channel} Old bank/program not possible in new soundfont. Switching to first available program.")
self.setPatch(channel, youchoose_bank, youchoose_program)
break #inner loop. one instrument is enough.
@ -249,7 +251,7 @@ class Sampler_sf2(Data):
cbox.JackIO.Metadata.set_pretty_name(portnameL, f"{str(channel).zfill(2)}-L : {name}")
cbox.JackIO.Metadata.set_pretty_name(portnameR, f"{str(channel).zfill(2)}-R : {name}")
except Exception as e: #No Jack Meta Data
logging.error(e)
logger.error(e)
def updateAllChannelJackMetadaPrettyname(self):

35
template/engine/sequencer.py

@ -19,7 +19,9 @@ 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 <http://www.gnu.org/licenses/>.
"""
import logging; logging.info("import {}".format(__file__))
import logging; logger = logging.getLogger(__name__); logger.info("import")
#Standard Library
from typing import List, Dict, Tuple, Iterable
@ -110,7 +112,7 @@ class Score(Data):
try:
cbox.JackIO.Metadata.set_all_port_order(order)
except Exception as e: #No Jack Meta Data
logging.error(e)
logger.error(e)
def trackById(self, trackId:int):
for track in self.tracks:
@ -318,7 +320,7 @@ class SequencerInterface(_Interface): #Basically the midi part of a track.
def _processAfterInit(self):
#Create midi out and cbox track
logging.info("Creating empty SequencerInterface instance")
logger.info("Creating empty SequencerInterface instance")
super()._processAfterInit()
self.cboxMidiOutUuid = cbox.JackIO.create_midi_output(self._name)
self.calfboxTrack.set_external_output(self.cboxMidiOutUuid)
@ -517,7 +519,7 @@ class TempoMap(object):
def __init__(self, parentData):
logging.info("Creating empty TempoMap instance")
logger.info("Creating empty TempoMap instance")
self.parentData = parentData
self._tempoMap = {0:(120.0, 4, 4)} # 4/4, 120bpm. will not be used on startup, but is needed if transportMaster is switched on
self._isTransportMaster = False
@ -550,7 +552,7 @@ class TempoMap(object):
@isTransportMaster.setter
def isTransportMaster(self, value:bool):
logging.info(f"Jack Transport Master status: {value}")
logger.info(f"Jack Transport Master status: {value}")
self._isTransportMaster = value
if value:
self._sendToCbox() #reactivate existing tempo map
@ -568,14 +570,13 @@ class TempoMap(object):
self._updatePlayback()
def _sanitize(self):
"""Inplace modification of self.tempoMap. Remove zeros and convert to float values.
"""
self._tempoMap = {int(key):(float(value), timesigNum, timesigDenom) for key, (value, timesigNum, timesigDenom) in self._tempoMap.items() if value > 0.0}
#Don't use the following. Empty tempo maps are allowed, especially in jack transport slave mode.
#Instead set a default tempo 120 on init explicitly
#if not self._tempoMap:
# logging.error("Found invalid tempo map. Forcing to 120 bpm. Please correct manually")
# self._tempoMap = {0, 120.0}
"""Inplace modification of self.tempoMap. Remove zeros and convert to float values. """
self._tempoMap = {int(key):(float(value), timesigNum, timesigDenom) for key, (value,
timesigNum, timesigDenom) in self._tempoMap.items() if value > 0.0} #Don't use the
following. Empty tempo maps are allowed, especially in jack transport slave mode. #Instead
set a default tempo 120 on init explicitly #if not self._tempoMap: # logger.error("Found
invalid tempo map. Forcing to 120 bpm. Please correct manually") # self._tempoMap = {0,
120.0}
def _clearCboxTempoMap(self):
"""Remove all cbox tempo values by iterating over all of them and set them to None, which is
@ -623,9 +624,9 @@ class TempoMap(object):
def setTimeSignature(self, timesigNum:int, timesigDenom:int):
"""Simple traditional timesig setter. Overrides all other timesig data.
Works in tandem with self.setTimeSignature.
"""
"""
assert timesigNum > 0, timesigNum
assert timesigDenom in traditionalNumberToBaseDuration, timesigDenom
#assert timesigDenom in traditionalNumberToBaseDuration, (timesigDenom, traditionalNumberToBaseDuration) For Laborejo this makes sense, but Patroneo has a fallback option with irregular timesigs: 8 steps in groups of 3 to make a quarter is valid. Results in "8/12"
currentValue, OLD_timesigNum, OLD_timesigDenom = self._tempoMap[0]
self.setTempoMap({0:(currentValue, timesigNum, timesigDenom)})
@ -636,7 +637,7 @@ class TempoMap(object):
assert 0 in self._tempoMap, self._tempoMap
return self._tempoMap[0][0] #second [0] is the tuple (tempo, timesig, timesig)
else:
logging.info("Requested Quarter Notes per Minute, but we are not transport master")
logger.info("Requested Quarter Notes per Minute, but we are not transport master")
return None
#Save / Load / Export
@ -650,7 +651,7 @@ class TempoMap(object):
@classmethod
def instanceFromSerializedData(cls, parentData, serializedData):
logging.info("Loading TempoMap from saved file")
logger.info("Loading TempoMap from saved file")
self = cls.__new__(cls)
self.parentData = parentData
self._tempoMap = serializedData["tempoMap"] #json saves dict-keys as strings. We revert back in sanitize()

29
template/engine/session.py

@ -19,7 +19,8 @@ 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 <http://www.gnu.org/licenses/>.
"""
import logging; logging.info("import {}".format(__file__))
import logging; logger = logging.getLogger(__name__); logger.info("import")
#Python Standard Library
from warnings import warn
@ -82,7 +83,7 @@ class Session(object):
return jsonDataAsString.replace(self.sessionPrefix, "<sessionDirectory>")
def nsm_openOrNewCallback(self, ourPath, sessionName, ourClientNameUnderNSM):
logging.info("New/Open session")
logger.info("New/Open session")
cbox.init_engine("")
#Most set config must be called before start audio. Set audio outputs seems to be an exception.
@ -110,13 +111,13 @@ class Session(object):
self.data = None #This makes debugging output nicer. If we init Data() here all errors will be presented as follow-up error "while handling exception FileNotFoundError".
except (NotADirectoryError, PermissionError) as e:
self.data = None
logging.error("Will not load or save because: " + e.__repr__())
logger.error("Will not load or save because: " + e.__repr__())
if not self.data:
self.data = Data(parentSession = self)
logging.info("New/Open session complete")
logger.info("New/Open session complete")
def openFromJson(self, absoluteJsonFilePath):
logging.info("Loading file start")
logger.info("Loading file start")
with open(absoluteJsonFilePath, "r", encoding="utf-8") as f:
try:
text = self.addSessionPrefix(f.read())
@ -129,7 +130,7 @@ class Session(object):
self.guiWasSavedAsNSMVisible = result["guiWasSavedAsNSMVisible"]
self.guiSharedDataToSave = result["guiSharedDataToSave"]
assert type(self.guiSharedDataToSave) is dict, self.guiSharedDataToSave
logging.info("Loading file complete")
logger.info("Loading file complete")
return Data.instanceFromSerializedData(parentSession=self, serializedData=result)
else:
warn(f"""{absoluteJsonFilePath} was saved with {result["version"]} but we need {METADATA["version"]}""")
@ -145,7 +146,7 @@ class Session(object):
if not os.path.exists(ourPath):
os.makedirs(ourPath)
except Exception as e:
logging.error("Will not load or save because: " + e.__repr__())
logger.error("Will not load or save because: " + e.__repr__())
result = self.data.serialize()
result["origin"] = METADATA["url"]
@ -161,19 +162,21 @@ class Session(object):
with open(self.absoluteJsonFilePath, "w", encoding="utf-8") as f:
f.write(jsonData)
except Exception as e:
logging.error("Will not load or save because: " + e.__repr__())
logger.error("Will not load or save because: " + e.__repr__())
logger.info("Saving file complete")
return self.absoluteJsonFilePath
def stopSession(self):
"""This got registered with atexit in the nsm new or open callback above.
will handle all python exceptions, but not segfaults of C modules. """
logging.info("Starting Quit through @atexit, session.stopSession")
logger.info("Starting Quit through @atexit, session.stopSession")
self.eventLoop.stop()
logging.info("@atexit: Event loop stopped")
logger.info("@atexit: Event loop stopped")
#Don't do that. We are just a client.
#cbox.Transport.stop()
#logging.info("@atexit: Calfbox Transport stopped ")
#logger.info("@atexit: Calfbox Transport stopped ")
cbox.stop_audio()
logging.info("@atexit: Calfbox Audio stopped ")
logger.info("@atexit: Calfbox Audio stopped ")
cbox.shutdown_engine()
logging.info("@atexit: Calfbox Engine shutdown ")
logger.info("@atexit: Calfbox Engine shutdown ")

2
template/qtgui/about.py

@ -20,7 +20,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging; logging.info("import {}".format(__file__))
import logging; logger = logging.getLogger(__name__); logger.info("import")
#Standard Lib
from random import choice

5
template/qtgui/chooseSessionDirectory.py

@ -20,7 +20,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging; logging.info("import {}".format(__file__))
import logging; logger = logging.getLogger(__name__); logger.info("import")
#Standard Lib
from tempfile import gettempdir
@ -44,7 +45,7 @@ class ChooseSessionDirectory(QtWidgets.QDialog):
def __init__(self, qtApp):
language = QtCore.QLocale().languageToString(QtCore.QLocale().language())
logging.info("{}: Language set to {}".format(METADATA["name"], language))
logger.info("{}: Language set to {}".format(METADATA["name"], language))
if language in METADATA["supportedLanguages"]:
templateTranslator = QtCore.QTranslator()
templateTranslator.load(METADATA["supportedLanguages"][language], ":/template/translations/") #colon to make it a resource URL

3
template/qtgui/constantsAndConfigs.py

@ -20,7 +20,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging; logging.info("import {}".format(__file__))
import logging; logger = logging.getLogger(__name__); logger.info("import")
#Third Party Modules
from PyQt5 import QtWidgets, QtCore, QtGui

5
template/qtgui/debugScript.py

@ -20,6 +20,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging; logger = logging.getLogger(__name__); logger.info("import")
import os.path
#from code import InteractiveInterpreter
import logging
@ -41,7 +44,7 @@ class DebugScriptRunner(object):
def _createEmptyDebugScript(self):
assert self.absoluteScriptFilePath
logging.info(f"{self.nsmClient.ourClientNameUnderNSM}: Script file not found. Initializing: {self.absoluteScriptFilePath}")
logger.info(f"{self.nsmClient.ourClientNameUnderNSM}: Script file not found. Initializing: {self.absoluteScriptFilePath}")
text = ("""#! /usr/bin/env python3"""
"\n"
"""# -*- coding: utf-8 -*-"""

16
template/qtgui/mainwindow.py

@ -20,7 +20,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging; logging.info("import {}".format(__file__))
import logging; logger = logging.getLogger(__name__); logger.info("import")
#Standard Library
import os
@ -95,15 +95,15 @@ class EventLoop(object):
def start(self):
"""The event loop MUST be started after the Qt Application instance creation"""
logging.info("Starting fast qt event loop")
logger.info("Starting fast qt event loop")
self.fastLoop.start(20)
logging.info("Starting slow qt event loop")
logger.info("Starting slow qt event loop")
self.slowLoop.start(100)
def stop(self):
logging.info("Stopping fast qt event loop")
logger.info("Stopping fast qt event loop")
self.fastLoop.stop()
logging.info("Stopping slow qt event loop")
logger.info("Stopping slow qt event loop")
self.slowLoop.stop()
api.session.eventLoop = EventLoop()
@ -113,7 +113,7 @@ api.session.eventLoop = EventLoop()
#Setup the translator before classes are set up. Otherwise we can't use non-template translation.
#to test use LANGUAGE=de_DE.UTF-8 . not LANG=
language = QtCore.QLocale().languageToString(QtCore.QLocale().language())
logging.info("{}: Language set to {}".format(METADATA["name"], language))
logger.info("{}: Language set to {}".format(METADATA["name"], language))
if language in METADATA["supportedLanguages"]:
templateTranslator = QtCore.QTranslator()
templateTranslator.load(METADATA["supportedLanguages"][language], ":/template/translations/") #colon to make it a resource URL
@ -355,13 +355,13 @@ class MainWindow(QtWidgets.QMainWindow):
#Close and exit
def _nsmQuit(self, ourPath, sessionName, ourClientNameUnderNSM):
logging.info("Qt main window received NSM exit callback. Calling pythons system exit. ")
logger.info("Qt main window received NSM exit callback. Calling pythons system exit. ")
self.storeWindowSettings()
#api.stopEngine() #will be called trough sessions atexit
#self.qtApp.quit() #does not work. This will fail and pynsmclient2 will send SIGKILL
sysexit() #works, NSM cleanly detects a quit. Triggers the session atexit condition
logging.error("Code executed after sysexit. This message should not have been visible.")
logger.error("Code executed after sysexit. This message should not have been visible.")
#Code here never gets executed.
def closeEvent(self, event):

3
template/qtgui/menu.py

@ -20,7 +20,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging; logging.info("import {}".format(__file__))
import logging; logger = logging.getLogger(__name__); logger.info("import")
#Standard Library

183
template/qtgui/nsmclient.py

@ -1,7 +1,7 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
PyNSMClient 2.1 - A Non Session Manager Client-Library in one file.
PyNSMClient - A Non Session Manager Client-Library in one file.
The Non-Session-Manager by Jonathan Moore Liles <male@tuxfamily.org>: http://non.tuxfamily.org/nsm/
With help from code fragments from https://github.com/attwad/python-osc ( DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE v2 )
@ -28,6 +28,8 @@ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
import logging;
logger = None #filled by init with prettyName
import struct
import socket
@ -37,7 +39,6 @@ import os.path
import shutil
from uuid import uuid4
from sys import argv
import logging
from signal import signal, SIGTERM, SIGINT, SIGKILL #react to exit signals to close the client gracefully. Or kill if the client fails to do so.
from urllib.parse import urlparse
@ -173,13 +174,13 @@ class _IncomingMessage(object):
elif param == "s": # String.
val, index = self.get_string(self._dgram, index)
else:
logging.warning("pynsm2: Unhandled parameter type: {0}".format(param))
logger.warning("Unhandled parameter type: {0}".format(param))
continue
self._parameters.append(val)
except ValueError as pe:
#raise ValueError('Found incorrect datagram, ignoring it', pe)
# Raising an error is not ignoring it!
logging.warning("pynsm2: Found incorrect datagram, ignoring it. {}".format(pe))
logger.warning("Found incorrect datagram, ignoring it. {}".format(pe))
@property
def oscpath(self):
@ -259,18 +260,21 @@ class NSMClient(object):
Does not run an event loop itself and depends on the host loop.
E.g. a Qt timer or just a simple while True: sleep(0.1) in Python."""
def __init__(self, prettyName, supportsSaveStatus, saveCallback, openOrNewCallback, exitProgramCallback, hideGUICallback=None, showGUICallback=None, broadcastCallback=None, loggingLevel = "info"):
def __init__(self, prettyName, supportsSaveStatus, saveCallback, openOrNewCallback, exitProgramCallback, hideGUICallback=None, showGUICallback=None, broadcastCallback=None, sessionIsLoadedCallback=None, loggingLevel = "info"):
self.nsmOSCUrl = self.getNsmOSCUrl() #this fails and raises NSMNotRunningError if NSM is not available. Host programs can ignore it or exit their program.
self.realClient = True
self.cachedSaveStatus = True #save status checks for this.
self.cachedSaveStatus = None #save status checks for this.
global logger
logger = logging.getLogger(prettyName)
logger.info("import")
if loggingLevel == "info" or loggingLevel == 20:
logging.getLogger().setLevel(logging.INFO) #development
logging.info(prettyName + ":pynsm2: Starting PyNSM2 Client with logging level INFO. Switch to 'error' for a release!") #the NSM name is not ready yet so we just use the pretty name
logging.basicConfig(level=logging.INFO) #development
logger.info("Starting PyNSM2 Client with logging level INFO. Switch to 'error' for a release!") #the NSM name is not ready yet so we just use the pretty name
elif loggingLevel == "error" or loggingLevel == 40:
logging.getLogger().setLevel(logging.ERROR) #production
logging.basicConfig(level=logging.ERROR) #production
else:
raise ValueError("Unknown logging level: {}. Choose 'info' or 'error'".format(loggingLevel))
@ -281,23 +285,30 @@ class NSMClient(object):
self.saveCallback = saveCallback
self.exitProgramCallback = exitProgramCallback
self.openOrNewCallback = openOrNewCallback #The host needs to: Create a jack client with ourClientNameUnderNSM - Open the saved file and all its resources
self.broadcastCallback = broadcastCallback if broadcastCallback else None
self.hideGUICallback = hideGUICallback if hideGUICallback else None #if this stays None we don't ever need to check for it. This function will never be called by NSM anyway.
self.showGUICallback = showGUICallback if showGUICallback else None #if this stays None we don't ever need to check for it. This function will never be called by NSM anyway.
self.broadcastCallback = broadcastCallback
self.hideGUICallback = hideGUICallback
self.showGUICallback = showGUICallback
self.sessionIsLoadedCallback = sessionIsLoadedCallback
#Reactions get the raw _IncomingMessage OSC object
#A client can add to reactions.
self.reactions = {
"/nsm/client/save" : self._saveCallback,
"/nsm/client/show_optional_gui" : self.showGUICallback,
"/nsm/client/hide_optional_gui" : self.hideGUICallback,
"/nsm/client/show_optional_gui" : lambda msg: self.showGUICallback(),
"/nsm/client/hide_optional_gui" : lambda msg: self.hideGUICallback(),
"/nsm/client/session_is_loaded" : self._sessionIsLoadedCallback,
#Hello source-code reader. You can add your own reactions here by nsmClient.reactions[oscpath]=func, where func gets the raw _IncomingMessage OSC object as argument.
#broadcast is handled directly by the function because it has more parameters
}
self.discardReactions = set(["/nsm/client/session_is_loaded"])
#self.discardReactions = set(["/nsm/client/session_is_loaded"])
self.discardReactions = set()
#Networking and Init
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #internet, udp
self.sock.bind(('', 0)) #pick a free port on localhost.
ip, port = self.sock.getsockname()
self.ourOscUrl = f"osc.udp://{ip}:{port}/"
self.executableName = self.getExecutableName()
@ -322,6 +333,50 @@ class NSMClient(object):
self.sock.setblocking(False) #We have waited for tha handshake. Now switch blocking off because we expect sock.recvfrom to be empty in 99.99...% of the time so we shouldn't wait for the answer.
#After this point the host must include self.reactToMessage in its event loop
#We assume we are save at startup.
self.announceSaveStatus(isClean = True)
def reactToMessage(self):
"""This is the main loop message. It is added to the clients event loop."""
try:
data, addr = self.sock.recvfrom(4096) #4096 is quite big. We don't expect nsm messages this big. Better safe than sorry. However, messages will crash the program if they are bigger than 4096.
except BlockingIOError: #happens while no data is received. Has nothing to do with blocking or not.
return None
msg = _IncomingMessage(data)
if msg.oscpath in self.reactions:
self.reactions[msg.oscpath](msg)
elif msg.oscpath in self.discardReactions:
pass
elif msg.oscpath == "/reply" and msg.params == ["/nsm/server/open", "Loaded."]: #NSM sends that all programs of the session were loaded.
logger.info ("Got /reply Loaded from NSM Server")
elif msg.oscpath == "/reply" and msg.params == ["/nsm/server/save", "Saved."]: #NSM sends that all program-states are saved. Does only happen from the general save instruction, not when saving our client individually
logger.info ("Got /reply Saved from NSM Server")
elif msg.isBroadcast:
if self.broadcastCallback:
logger.info (f"Got broadcast with messagePath {msg.oscpath} and listOfArguments {msg.params}")
self.broadcastCallback(self.ourPath, self.sessionName, self.ourClientNameUnderNSM, msg.oscpath, msg.params)
else:
logger.info (f"No callback for broadcast! Got messagePath {msg.oscpath} and listOfArguments {msg.params}")
elif msg.oscpath == "/error":
logger.warning("Got /error from NSM Server. Path: {} , Parameter: {}".format(msg.oscpath, msg.params))
else:
logger.warning("Reaction not implemented:. Path: {} , Parameter: {}".format(msg.oscpath, msg.params))
def send(self, path:str, listOfParameters:list, host=None, port=None):
"""Send any osc message. Defaults to nsmd URL.
Will not wait for an answer but return None."""
if host and port:
url = (host, port)
else:
url = self.nsmOSCUrl
msg = _OutgoingMessage(path)
for arg in listOfParameters:
msg.add_arg(arg) #type is auto-determined by outgoing message
self.sock.sendto(msg.build(), url)
def getNsmOSCUrl(self):
"""Return and save the nsm osc url or raise an error"""
nsmOSCUrl = getenv("NSM_URL")
@ -381,13 +436,13 @@ class NSMClient(object):
if msg.oscpath == "/error":
originalMessage, errorCode, reason = msg.params
logging.error("Code {}: {}".format(errorCode, reason))
logger.error("Code {}: {}".format(errorCode, reason))
quit()
elif msg.oscpath == "/reply":
nsmAnnouncePath, welcomeMessage, managerName, self.serverFeatures = msg.params
assert nsmAnnouncePath == "/nsm/server/announce", nsmAnnouncePath
logging.info(self.prettyName + ":pynsm2: Got /reply " + welcomeMessage)
logger.info("Got /reply " + welcomeMessage)
#Wait for /nsm/client/open
data, addr = self.sock.recvfrom(1024)
@ -395,9 +450,9 @@ class NSMClient(object):
assert msg.oscpath == "/nsm/client/open", msg.oscpath
self.ourPath, self.sessionName, self.ourClientNameUnderNSM = msg.params
self.ourClientId = os.path.splitext(self.ourClientNameUnderNSM)[1][1:]
logging.info(self.ourClientNameUnderNSM + ":pynsm2: Got '/nsm/client/open' from NSM. Telling our client to load or create a file with name {}".format(self.ourPath))
logger.info("Got '/nsm/client/open' from NSM. Telling our client to load or create a file with name {}".format(self.ourPath))
self.openOrNewCallback(self.ourPath, self.sessionName, self.ourClientNameUnderNSM) #Host function to either load an existing session or create a new one.
logging.info(self.ourClientNameUnderNSM + ":pynsm2: Our client should be done loading or creating the file {}".format(self.ourPath))
logger.info("Our client should be done loading or creating the file {}".format(self.ourPath))
replyToOpen = _OutgoingMessage("/reply")
replyToOpen.add_arg("/nsm/client/open")
replyToOpen.add_arg("{} is opened or created".format(self.prettyName))
@ -409,20 +464,23 @@ class NSMClient(object):
message = "/nsm/client/gui_is_shown" if isVisible else "/nsm/client/gui_is_hidden"
self.isVisible = isVisible
guiVisibility = _OutgoingMessage(message)
logging.info(self.ourClientNameUnderNSM + ":pynsm2: Telling NSM that our clients switched GUI visibility to: {}".format(message))
logger.info("Telling NSM that our clients switched GUI visibility to: {}".format(message))
self.sock.sendto(guiVisibility.build(), self.nsmOSCUrl)
def announceSaveStatus(self, isClean):
"""Only send to the NSM Server if there was really a change"""
if not self.supportsSaveStatus:
return
if not isClean == self.cachedSaveStatus:
message = "/nsm/client/is_clean" if isClean else "/nsm/client/is_dirty"
self.cachedSaveStatus = isClean
saveStatus = _OutgoingMessage(message)
logging.info(self.ourClientNameUnderNSM + ":pynsm2: Telling NSM that our clients save state is now: {}".format(message))
logger.info("Telling NSM that our clients save state is now: {}".format(message))
self.sock.sendto(saveStatus.build(), self.nsmOSCUrl)
def _saveCallback(self):
logging.info(self.ourClientNameUnderNSM + ":pynsm2: Telling our client to save as {}".format(self.ourPath))
def _saveCallback(self, msg):
logger.info("Telling our client to save as {}".format(self.ourPath))
self.saveCallback(self.ourPath, self.sessionName, self.ourClientNameUnderNSM)
replyToSave = _OutgoingMessage("/reply")
replyToSave.add_arg("/nsm/client/save")
@ -431,31 +489,11 @@ class NSMClient(object):
#it is assumed that after saving the state is clear
self.announceSaveStatus(isClean = True)
def reactToMessage(self):
try:
data, addr = self.sock.recvfrom(4096) #4096 is quite big. We don't expect nsm messages this big. Better safe than sorry. See next lines comment
except BlockingIOError: #happens while no data is received. Has nothing to do with blocking or not.
return None
msg = _IncomingMessage(data) #However, messages will crash the program if they are bigger than 4096.
if msg.oscpath in self.reactions:
self.reactions[msg.oscpath]()
elif msg.oscpath in self.discardReactions:
pass
elif msg.oscpath == "/reply" and msg.params == ["/nsm/server/open", "Loaded."]: #NSM sends that all programs of the session were loaded.
logging.info (self.ourClientNameUnderNSM + ":pynsm2: Got /reply Loaded from NSM Server")
elif msg.oscpath == "/reply" and msg.params == ["/nsm/server/save", "Saved."]: #NSM sends that all program-states are saved. Does only happen from the general save instruction, not when saving our client individually
logging.info (self.ourClientNameUnderNSM + ":pynsm2: Got /reply Saved from NSM Server")
elif msg.isBroadcast:
if self.broadcastCallback:
logging.info (self.ourClientNameUnderNSM + f":pynsm2: Got broadcast with messagePath {msg.oscpath} and listOfArguments {msg.params}")
self.broadcastCallback(self.ourPath, self.sessionName, self.ourClientNameUnderNSM, msg.oscpath, msg.params)
else:
logging.info (self.ourClientNameUnderNSM + f":pynsm2: No callback for broadcast! Got messagePath {msg.oscpath} and listOfArguments {msg.params}")
elif msg.oscpath == "/error":
logging.warning(self.ourClientNameUnderNSM + ":pynsm2: Got /error from NSM Server. Path: {} , Parameter: {}".format(msg.oscpath, msg.params))
else:
logging.warning(self.ourClientNameUnderNSM + ":pynsm2: Reaction not implemented:. Path: {} , Parameter: {}".format(msg.oscpath, msg.params))
def _sessionIsLoadedCallback(self, msg):
if self.sessionIsLoadedCallback:
logger.info("Telling our client that the session has finished loading")
self.sessionIsLoadedCallback()
def sigtermHandler(self, signal, frame):
"""Wait for the user to quit the program
@ -471,33 +509,33 @@ class NSMClient(object):
gdb --args python foo.py
the Python signal handler will not work. This has nothing to do with this library.
"""
logging.info(self.ourClientNameUnderNSM + ":pynsm2: Telling our client to quit.")
logger.info("Telling our client to quit.")
self.exitProgramCallback(self.ourPath, self.sessionName, self.ourClientNameUnderNSM)
#There is a chance that exitProgramCallback will hang and the program won't quit. However, this is broken design and bad programming. We COULD place a timeout here and just kill after 10s or so, but that would make quitting our responsibility and fixing a broken thing.
#If we reach this point we have reached the point of no return. Say goodbye.
logging.warning(self.ourClientNameUnderNSM + ":pynsm2: Client did not quit on its own. Sending SIGKILL.")
logger.warning("Client did not quit on its own. Sending SIGKILL.")
kill(getpid(), SIGKILL)
logging.error(self.ourClientNameUnderNSM + ":pynsm2: pynsm2: SIGKILL did nothing. Do it manually.")
logger.error("SIGKILL did nothing. Do it manually.")
def debugResetDataAndExit(self):
"""This is solely meant for debugging and testing. The user way of action should be to
remove the client from the session and add a new instance, which will get a different
NSM-ID.
Afterwards we perform a clean exit."""
logging.warning(self.ourClientNameUnderNSM + ":pynsm2: debugResetDataAndExit will now delete {} and then request an exit.".format(self.ourPath))
logger.warning("debugResetDataAndExit will now delete {} and then request an exit.".format(self.ourPath))
if os.path.exists(self.ourPath):
if os.path.isfile(self.ourPath):
try:
os.remove(self.ourPath)
except Exception as e:
logging.info(e)
logger.info(e)
elif os.path.isdir(self.ourPath):
try:
shutil.rmtree(self.ourPath)
except Exception as e:
logging.info(e)
logger.info(e)
else:
logging.info(self.ourClientNameUnderNSM + ":pynsm2: {} does not exist.".format(self.ourPath))
logger.info("{} does not exist.".format(self.ourPath))
self.serverSendExitToSelf()
def serverSendExitToSelf(self):
@ -508,13 +546,13 @@ class NSMClient(object):
Using this method will not result in a NSM-"client died unexpectedly" message that usually
happens a client quits on its own. This message is harmless but may confuse a user."""
logging.info(self.ourClientNameUnderNSM + ":pynsm2: instructing the NSM-Server to send SIGTERM to ourselves.")
logger.info("instructing the NSM-Server to send SIGTERM to ourselves.")
if "server-control" in self.serverFeatures:
message = _OutgoingMessage("/nsm/server/stop")
message.add_arg("{}".format(self.ourClientId))
self.sock.sendto(message.build(), self.nsmOSCUrl)
else:
logging.warning(self.ourClientNameUnderNSM + ":pynsm2: ...but the NSM-Server does not support server control. Quitting on our own. Server only supports: {}".format(self.serverFeatures))
logger.warning("...but the NSM-Server does not support server control. Quitting on our own. Server only supports: {}".format(self.serverFeatures))
kill(getpid(), SIGTERM) #this calls the exit callback but nsm will output something like "client died unexpectedly."
def serverSendSaveToSelf(self):
@ -523,14 +561,14 @@ class NSMClient(object):
NSM server so our client thinks it received a Save instruction. This leads to a clean
state with a good saveStatus and no required extra functionality in the client."""
logging.info(self.ourClientNameUnderNSM + ":pynsm2: instructing the NSM-Server to send Save to ourselves.")
logger.info("instructing the NSM-Server to send Save to ourselves.")
if "server-control" in self.serverFeatures:
#message = _OutgoingMessage("/nsm/server/save") # "Save All" Command.
message = _OutgoingMessage("/nsm/gui/client/save")
message.add_arg("{}".format(self.ourClientId))
self.sock.sendto(message.build(), self.nsmOSCUrl)
else:
logging.warning(self.ourClientNameUnderNSM + ":pynsm2: ...but the NSM-Server does not support server control. Server only supports: {}".format(self.serverFeatures))
logger.warning("...but the NSM-Server does not support server control. Server only supports: {}".format(self.serverFeatures))
def changeLabel(self, label:str):
"""This function is implemented because it is provided by NSM. However, it does not much.
@ -539,7 +577,7 @@ class NSMClient(object):
We would have to send it every startup ourselves.
This is fine for us as clients, but you need to provide a GUI field to enter that label."""
logging.info(self.ourClientNameUnderNSM + ":pynsm2: Telling the NSM-Server that our label is now " + label)
logger.info("Telling the NSM-Server that our label is now " + label)
message = _OutgoingMessage("/nsm/client/label")
message.add_arg(label) #s:label
self.sock.sendto(message.build(), self.nsmOSCUrl)
@ -547,13 +585,18 @@ class NSMClient(object):
def broadcast(self, path:str, arguments:list):
"""/nsm/server/broadcast s:path [arguments...]
We, as sender, will not receive the broadcast back.
Broadcasts starting with /nsm are not allowed and will get discarded by the server
"""
logging.info(self.ourClientNameUnderNSM + ":pynsm2: Sending broadcast " + path + repr(arguments))
message = _OutgoingMessage("/nsm/server/broadcast")
message.add_arg(path)
for arg in arguments:
message.add_arg(arg) #type autodetect
self.sock.sendto(message.build(), self.nsmOSCUrl)
if path.startswith("/nsm"):
logger.warning("Attempted broadbast starting with /nsm. Not allwoed")
else:
logger.info("Sending broadcast " + path + repr(arguments))
message = _OutgoingMessage("/nsm/server/broadcast")
message.add_arg(path)
for arg in arguments:
message.add_arg(arg) #type autodetect
self.sock.sendto(message.build(), self.nsmOSCUrl)
def importResource(self, filePath):
"""aka. import into session
@ -614,14 +657,14 @@ class NSMClient(object):
if filePathInOurSession:
#loadResource from our session dir. Portable session, manually copied beforehand or just loading a link again.
linkedPath = filePath #we could return here, but we continue to get the tests below.
logging.info(self.ourClientNameUnderNSM + f":pynsm2: tried to import external resource {filePath} but this is already in our session directory. We use this file directly instead. ")
logger.info(f"tried to import external resource {filePath} but this is already in our session directory. We use this file directly instead. ")
elif linkedPathAlreadyExists and os.readlink(linkedPath) == filePath:
#the imported file already exists as link in our session dir. We do not link it again but simply report the existing link.
#We only check for the first target of the existing link and do not follow it through to a real file.
#This way all user abstractions and file structures will be honored.
linkedPath = linkedPath
logging.info(self.ourClientNameUnderNSM + f":pynsm2: tried to import external resource {filePath} but this was already linked to our session directory before. We use the old link: {linkedPath} ")
logger.info(f"tried to import external resource {filePath} but this was already linked to our session directory before. We use the old link: {linkedPath} ")
elif linkedPathAlreadyExists:
#A new file shall be imported but it would create a linked name which already exists in our session dir.
@ -630,13 +673,13 @@ class NSMClient(object):
uniqueLinkedPath = firstpart + "." + uuid4().hex + extension
assert not os.path.exists(uniqueLinkedPath)
os.symlink(filePath, uniqueLinkedPath)
logging.info(self.ourClientNameUnderNSM + f":pysm2: tried to import external resource {filePath} but potential target link {linkedPath} already exists. Linked to {uniqueLinkedPath} instead.")
logger.info(self.ourClientNameUnderNSM + f":pysm2: tried to import external resource {filePath} but potential target link {linkedPath} already exists. Linked to {uniqueLinkedPath} instead.")
linkedPath = uniqueLinkedPath
else: #this is the "normal" case. External resources will be linked.
assert not os.path.exists(linkedPath)
os.symlink(filePath, linkedPath)
logging.info(self.ourClientNameUnderNSM + f":pynsm2: imported external resource {filePath} as link {linkedPath}")
logger.info(f"imported external resource {filePath} as link {linkedPath}")
assert os.path.exists(linkedPath), linkedPath
return linkedPath

5
template/qtgui/nsmsingleserver.py

@ -20,7 +20,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging; logging.info("import {}".format(__file__))
import logging; logger = logging.getLogger(__name__); logger.info("import")
import os, socket, asyncio
from signal import signal, SIGTERM, SIGUSR1
from threading import Thread
@ -105,7 +106,7 @@ def startSingleNSMServer(directory):
#loop.run_forever()
#asyncio.run(asyncio.start_server(handle_client, 'localhost', SERVER_PORT))
logging.info(f"Starting fake NSM server on port {SERVER_PORT}")
logger.info(f"Starting fake NSM server on port {SERVER_PORT}")
#For Carla:
signal(SIGUSR1, NSMProtocol.staticSave)

3
template/qtgui/submenus.py

@ -20,6 +20,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging; logger = logging.getLogger(__name__); logger.info("import")
from typing import Iterable, Callable, Tuple
from PyQt5 import QtCore, QtGui, QtWidgets
import engine.api as api

3
template/qtgui/usermanual.py

@ -20,8 +20,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging; logging.info("import {}".format(__file__))
import logging; logger = logging.getLogger(__name__); logger.info("import")
#System Wide Modules
from PyQt5 import QtCore, QtWidgets, QtGui

38
template/start.py

@ -47,11 +47,15 @@ args = parser.parse_args()
import logging
if args.verbose:
logging.getLogger().setLevel(logging.INFO) #development
logging.basicConfig(level=logging.INFO) #development
#logging.getLogger().setLevel(logging.INFO) #development
else:
logging.getLogger().setLevel(logging.ERROR) #production
logging.basicConfig(level=logging.ERROR) #production
#logging.getLogger().setLevel(logging.ERROR) #production
logger = logging.getLogger(__name__)
logger.info("import")
logging.info("import {}".format(__file__))
"""set up python search path before the program starts and cbox gets imported.
We need to be earliest, so let's put it here.
@ -67,11 +71,11 @@ import os.path
try:
from compiledprefix import prefix
compiledVersion = True
logging.info("Compiled prefix found: {}".format(prefix))
logger.info("Compiled prefix found: {}".format(prefix))
except ModuleNotFoundError as e:
compiledVersion = False
logging.info("Compiled version: {}".format(compiledVersion))
logger.info("Compiled version: {}".format(compiledVersion))
cboxSharedObjectVersionedName = "lib"+METADATA["shortName"]+".so." + METADATA["version"]
@ -122,12 +126,12 @@ else:
if os.path.exists (os.path.join(_root, "site-packages", "calfbox", "cbox.py")):
#add to the front to have higher priority than system site-packages
logging.info("Will attempt to start with local calfbox python module: {}".format(os.path.join(_root, "site-packages", "calfbox", "cbox.py")))
logger.info("Will attempt to start with local calfbox python module: {}".format(os.path.join(_root, "site-packages", "calfbox", "cbox.py")))
sys.path.insert(0, os.path.join(os.path.join(_root, "site-packages")))
#else try to use system-wide calfbox. Check for this and if the .so exists at the end of this file.
logging.info("PATHS: {}".format(PATHS))
logger.info("PATHS: {}".format(PATHS))
def exitWithMessage(message:str):
@ -231,7 +235,7 @@ def profiler(*pargs, **kwds):
from tempfile import NamedTemporaryFile
cprofPath = NamedTemporaryFile().name + ".cprof"
pr.dump_stats(cprofPath)
logging.info("{}: write profiling data to {}".format(METADATA["name"], cprofPath))
logger.info("{}: write profiling data to {}".format(METADATA["name"], cprofPath))
print (f"pyprof2calltree -k -i {cprofPath}")
pr = cProfile.Profile()
@ -243,6 +247,18 @@ def profiler(*pargs, **kwds):
#Program execution
yield
#Catch Exceptions even if PyQt crashes.
import sys
sys._excepthook = sys.excepthook
def exception_hook(exctype, value, traceback):
"""This hook purely exists to call sys.exit(1) even on a Qt crash
so that atexit gets triggered"""
#print(exctype, value, traceback)
logger.error("Caught crash in execpthook. Trying too execute atexit anyway")
sys._excepthook(exctype, value, traceback)
sys.exit(1)
sys.excepthook = exception_hook
def startPseudoNSMServer(path):
from os import getenv
@ -288,13 +304,13 @@ if args.mute:
#Make sure calfbox is available.
if "CALFBOXLIBABSPATH" in os.environ:
logging.info("Looking for calfbox shared library in absolute path: {}".format(os.environ["CALFBOXLIBABSPATH"]))
logger.info("Looking for calfbox shared library in absolute path: {}".format(os.environ["CALFBOXLIBABSPATH"]))
else:
logging.info("Looking for calfbox shared library systemwide through ctypes.util.find_library")
logger.info("Looking for calfbox shared library systemwide through ctypes.util.find_library")
try:
from calfbox import cbox
logging.info("{}: using cbox python module from {} . Local version has higher priority than system wide.".format(METADATA["name"], os.path.abspath(cbox.__file__)))
logger.info("{}: using cbox python module from {} . Local version has higher priority than system wide.".format(METADATA["name"], os.path.abspath(cbox.__file__)))
except Exception as e:
print (e)

Loading…
Cancel
Save