From 6c3218ee055651375c7c7d972653de6ec1692f49 Mon Sep 17 00:00:00 2001 From: Nils <> Date: Fri, 10 Apr 2020 23:23:57 +0200 Subject: [PATCH] Better logging with file names --- engine/__init__.py | 2 +- engine/main.py | 2 +- engine/pattern.py | 1 + engine/track.py | 2 + qtgui/constantsAndConfigs.py | 2 +- qtgui/mainwindow.py | 48 +++--- qtgui/pattern_grid.py | 3 + qtgui/songeditor.py | 2 + qtgui/timeline.py | 2 + template/calfbox/py/_cbox2.py | 2 +- template/engine/api.py | 4 +- template/engine/data.py | 4 +- template/engine/duration.py | 6 +- template/engine/history.py | 4 +- template/engine/input_midi.py | 4 +- template/engine/metronome.py | 4 +- template/engine/midi.py | 4 +- template/engine/pitch.py | 4 +- template/engine/sampler_sf2.py | 14 +- template/engine/sequencer.py | 18 ++- template/engine/session.py | 27 ++-- template/qtgui/about.py | 2 +- template/qtgui/chooseSessionDirectory.py | 5 +- template/qtgui/constantsAndConfigs.py | 3 +- template/qtgui/debugScript.py | 5 +- template/qtgui/mainwindow.py | 16 +- template/qtgui/menu.py | 3 +- template/qtgui/nsmclient.py | 177 ++++++++++++++--------- template/qtgui/nsmsingleserver.py | 5 +- template/qtgui/submenus.py | 3 + template/qtgui/usermanual.py | 3 +- template/start.py | 38 +++-- 32 files changed, 256 insertions(+), 163 deletions(-) diff --git a/engine/__init__.py b/engine/__init__.py index 301e00d..2c2d148 100644 --- a/engine/__init__.py +++ b/engine/__init__.py @@ -20,7 +20,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import logging; logging.info("import {}".format(__file__)) +import logging; logger = logging.getLogger(__name__); logger.info("import") #This file only exists as a reminder to _not_ create it again wrongly in the future. diff --git a/engine/main.py b/engine/main.py index f9c1f83..80bb2ae 100644 --- a/engine/main.py +++ b/engine/main.py @@ -20,7 +20,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import logging; logging.info("import {}".format(__file__)) +import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library Modules diff --git a/engine/pattern.py b/engine/pattern.py index e658027..34ce8dd 100644 --- a/engine/pattern.py +++ b/engine/pattern.py @@ -19,6 +19,7 @@ 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 Modules from typing import List, Set, Dict, Tuple diff --git a/engine/track.py b/engine/track.py index 525b5ad..49cbf20 100644 --- a/engine/track.py +++ b/engine/track.py @@ -20,6 +20,8 @@ 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 Modules from typing import List, Set, Dict, Tuple diff --git a/qtgui/constantsAndConfigs.py b/qtgui/constantsAndConfigs.py index 6541b63..7d73a8a 100644 --- a/qtgui/constantsAndConfigs.py +++ b/qtgui/constantsAndConfigs.py @@ -20,7 +20,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import logging; logging.info("import {}".format(__file__)) +import logging; logger = logging.getLogger(__name__); logger.info("import") from template.qtgui.constantsAndConfigs import ConstantsAndConfigs as TemplateConstantsAndConfigs diff --git a/qtgui/mainwindow.py b/qtgui/mainwindow.py index acf7db1..3cf60a9 100644 --- a/qtgui/mainwindow.py +++ b/qtgui/mainwindow.py @@ -19,7 +19,7 @@ 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; logging.info("import {}".format(__file__)) +import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library Modules import os.path @@ -109,25 +109,25 @@ class MainWindow(TemplateMainWindow): self.ui.toStartButton.setText("") self.ui.toStartButton.setIcon(QtGui.QIcon(':tostart.png')) self.ui.toStartButton.setToolTip(QtCore.QCoreApplication.translate("PlaybackControls", "[Home] Jump to Start")) - self.ui.toStartButton.clicked.connect(api.toStart) + self.ui.toStartButton.clicked.connect(api.rewind) self.ui.centralwidget.addAction(self.ui.actionToStart) #no action without connection to a widget. self.ui.actionToStart.triggered.connect(self.ui.toStartButton.click) - ##Song Editor - self.ui.songEditorView.parentMainWindow = self - self.songEditor = SongEditor(parentView=self.ui.songEditorView) + ##Song Editor + self.ui.songEditorView.parentMainWindow = self + self.songEditor = SongEditor(parentView=self.ui.songEditorView) self.ui.songEditorView.setScene(self.songEditor) - self.ui.songEditorView.setViewport(QtWidgets.QOpenGLWidget()) - - self.ui.trackEditorView.parentMainWindow = self + self.ui.songEditorView.setViewport(QtWidgets.QOpenGLWidget()) + + self.ui.trackEditorView.parentMainWindow = self self.trackLabelEditor = TrackLabelEditor(parentView=self.ui.trackEditorView) self.ui.trackEditorView.setScene(self.trackLabelEditor) - self.ui.trackEditorView.setViewport(QtWidgets.QOpenGLWidget()) + self.ui.trackEditorView.setViewport(QtWidgets.QOpenGLWidget()) - self.ui.timelineView.parentMainWindow = self + self.ui.timelineView.parentMainWindow = self self.timeline = Timeline(parentView=self.ui.timelineView) self.ui.timelineView.setScene(self.timeline) - self.ui.timelineView.setViewport(QtWidgets.QOpenGLWidget()) + self.ui.timelineView.setViewport(QtWidgets.QOpenGLWidget()) #Sync the vertical trackEditorView scrollbar (which is never shown) with the songEditorView scrollbar. self.ui.songEditorView.setVerticalScrollBar(self.ui.trackEditorView.verticalScrollBar()) #this seems backwards, but it is correct :) @@ -135,9 +135,9 @@ class MainWindow(TemplateMainWindow): #Sync the horizontal timelineView scrollbar (which is never shown) with the songEditorView scrollbar. self.ui.songEditorView.setHorizontalScrollBar(self.ui.timelineView.horizontalScrollBar()) #this seems backwards, but it is correct :) - ##Pattern Editor + ##Pattern Editor self.ui.gridView.parentMainWindow = self - + self.patternGrid = PatternGrid(parentView=self.ui.gridView) self.ui.gridView.setScene(self.patternGrid) self.ui.gridView.setRenderHints(QtGui.QPainter.TextAntialiasing) @@ -163,7 +163,7 @@ class MainWindow(TemplateMainWindow): #There is always a track. Forcing that to be active is better than having to hide all the pattern widgets, or to disable them. #However, we need the engine to be ready. self.chooseCurrentTrack(api.session.data.tracks[0].export()) #By Grabthar's hammer, by the suns of Worvan, what a hack! #TODO: Access to the sessions data structure directly instead of api. Not good. Getter function or api @property is cleaner. - + self.ui.gridView.horizontalScrollBar().setSliderPosition(0) self.ui.gridView.verticalScrollBar().setSliderPosition(0) @@ -215,15 +215,15 @@ class MainWindow(TemplateMainWindow): newTrackExporDict = api.createSiblingTrack(self.currentTrackId) self.chooseCurrentTrack(newTrackExporDict) - def _populatePatternToolbar(self): + def _populatePatternToolbar(self): self.patternToolbar.contextMenuEvent = api.nothing #remove that annoying Checkbox context menu - + spacerItemLeft = QtWidgets.QWidget() spacerItemLeft.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) spacerItemRight = QtWidgets.QWidget() spacerItemRight.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) - self.patternToolbar.addWidget(spacerItemLeft) + self.patternToolbar.addWidget(spacerItemLeft) #spacerItemRight is added as last widget #Actual widgets @@ -232,11 +232,11 @@ class MainWindow(TemplateMainWindow): transposeControls = TransposeControls(parentScene=self.patternGrid) self.patternToolbar.addWidget(transposeControls) - + #Finally add a spacer to center all widgets - self.patternToolbar.addWidget(spacerItemRight) - - + self.patternToolbar.addWidget(spacerItemRight) + + def _populateToolbar(self): self.ui.toolBar.contextMenuEvent = api.nothing #remove that annoying Checkbox context menu @@ -483,6 +483,6 @@ class MainWindow(TemplateMainWindow): error= ("fail", "delete", "merge")[s.errorHandling.currentIndex()] api.convert_subdivisions(value, error) - #Override template functions - def _stretchXCoordinates(*args): pass - + #Override template functions + def _stretchXCoordinates(*args): pass + diff --git a/qtgui/pattern_grid.py b/qtgui/pattern_grid.py index b5d4108..bfb617d 100644 --- a/qtgui/pattern_grid.py +++ b/qtgui/pattern_grid.py @@ -18,6 +18,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 . """ + +import logging; logger = logging.getLogger(__name__); logger.info("import") + from time import time import engine.api as api #Session is already loaded and created, no duplication. from template.engine import pitch diff --git a/qtgui/songeditor.py b/qtgui/songeditor.py index c737e49..0ce4a78 100644 --- a/qtgui/songeditor.py +++ b/qtgui/songeditor.py @@ -19,6 +19,8 @@ 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") + from time import time import engine.api as api #Session is already loaded and created, no duplication. from PyQt5 import QtCore, QtGui, QtWidgets diff --git a/qtgui/timeline.py b/qtgui/timeline.py index f8a7ef6..37d6dca 100644 --- a/qtgui/timeline.py +++ b/qtgui/timeline.py @@ -19,6 +19,8 @@ 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") + SIZE_UNIT = 25 #this is in manual sync with songeditor.py SIZE_UNIT diff --git a/template/calfbox/py/_cbox2.py b/template/calfbox/py/_cbox2.py index 94cd182..0191cd5 100644 --- a/template/calfbox/py/_cbox2.py +++ b/template/calfbox/py/_cbox2.py @@ -100,7 +100,7 @@ def find_calfbox(): cblib = os.environ["CALFBOXLIBABSPATH"] else: cblib = find_library('calfbox') - logging.info("Loading calfbox shared library: %s" % (cblib)) + logger.info:("Loading calfbox shared library: %s" % (cblib)) cb = cdll.LoadLibrary(cblib) return cb diff --git a/template/engine/api.py b/template/engine/api.py index 5bde20e..c5d2e43 100644 --- a/template/engine/api.py +++ b/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 . """ -import logging; logging.info("import {}".format(__file__)) + +import logging; logger = logging.getLogger(__name__); logger.info("import") + #Python Standard Library import os.path diff --git a/template/engine/data.py b/template/engine/data.py index 5f108d2..d7df9a9 100644 --- a/template/engine/data.py +++ b/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 . """ -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 diff --git a/template/engine/duration.py b/template/engine/duration.py index 57aa894..246fe7e 100644 --- a/template/engine/duration.py +++ b/template/engine/duration.py @@ -24,7 +24,9 @@ along with this program. If not, see . 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 diff --git a/template/engine/history.py b/template/engine/history.py index f39e267..99efd63 100644 --- a/template/engine/history.py +++ b/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 . """ -import logging; logging.info("import {}".format(__file__)) + +import logging; logger = logging.getLogger(__name__); logger.info("import") + from contextlib import contextmanager diff --git a/template/engine/input_midi.py b/template/engine/input_midi.py index 50f41ef..3685f4b 100644 --- a/template/engine/input_midi.py +++ b/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 . """ -import logging; logging.info("import {}".format(__file__)) + +import logging; logger = logging.getLogger(__name__); logger.info("import") + #Python Standard Library diff --git a/template/engine/metronome.py b/template/engine/metronome.py index a07e6d2..93f7582 100644 --- a/template/engine/metronome.py +++ b/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 . """ -import logging; logging.info("import {}".format(__file__)) + +import logging; logger = logging.getLogger(__name__); logger.info("import") + #Standard Library Modules from typing import Tuple diff --git a/template/engine/midi.py b/template/engine/midi.py index bdfb3c8..bf30555 100644 --- a/template/engine/midi.py +++ b/template/engine/midi.py @@ -24,7 +24,9 @@ along with this program. If not, see . 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 diff --git a/template/engine/pitch.py b/template/engine/pitch.py index 6623e21..4c154af 100644 --- a/template/engine/pitch.py +++ b/template/engine/pitch.py @@ -24,7 +24,9 @@ along with this program. If not, see . 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 diff --git a/template/engine/sampler_sf2.py b/template/engine/sampler_sf2.py index 3597b18..8f64898 100644 --- a/template/engine/sampler_sf2.py +++ b/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 . """ -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): diff --git a/template/engine/sequencer.py b/template/engine/sequencer.py index 075c37a..a8ec53e 100644 --- a/template/engine/sequencer.py +++ b/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 . """ -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 @@ -574,7 +576,7 @@ class TempoMap(object): #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") + # logger.error:("Found invalid tempo map. Forcing to 120 bpm. Please correct manually") # self._tempoMap = {0, 120.0} def _clearCboxTempoMap(self): @@ -636,7 +638,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 +652,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() diff --git a/template/engine/session.py b/template/engine/session.py index 3901391..941f07b 100644 --- a/template/engine/session.py +++ b/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 . """ -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, "") 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,19 @@ 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__()) 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 ") diff --git a/template/qtgui/about.py b/template/qtgui/about.py index 4958164..91b6603 100644 --- a/template/qtgui/about.py +++ b/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 . """ -import logging; logging.info("import {}".format(__file__)) +import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Lib from random import choice diff --git a/template/qtgui/chooseSessionDirectory.py b/template/qtgui/chooseSessionDirectory.py index 9bbe377..578751e 100644 --- a/template/qtgui/chooseSessionDirectory.py +++ b/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 . """ -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 diff --git a/template/qtgui/constantsAndConfigs.py b/template/qtgui/constantsAndConfigs.py index ccadec5..78b92a8 100644 --- a/template/qtgui/constantsAndConfigs.py +++ b/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 . """ -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 diff --git a/template/qtgui/debugScript.py b/template/qtgui/debugScript.py index c20418e..bab62ed 100644 --- a/template/qtgui/debugScript.py +++ b/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 . """ +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 -*-""" diff --git a/template/qtgui/mainwindow.py b/template/qtgui/mainwindow.py index 2b614fa..3ba9601 100644 --- a/template/qtgui/mainwindow.py +++ b/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 . """ -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): diff --git a/template/qtgui/menu.py b/template/qtgui/menu.py index 0201cc9..1bd9466 100644 --- a/template/qtgui/menu.py +++ b/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 . """ -import logging; logging.info("import {}".format(__file__)) +import logging; logger = logging.getLogger(__name__); logger.info("import") + #Standard Library diff --git a/template/qtgui/nsmclient.py b/template/qtgui/nsmclient.py index 60dcbe8..bf52e95 100644 --- a/template/qtgui/nsmclient.py +++ b/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 : 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,47 @@ 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 + + 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 +433,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 +447,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,7 +461,7 @@ 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): @@ -418,11 +470,11 @@ class NSMClient(object): 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 +483,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 +503,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 +540,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 +555,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 +571,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 +579,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 +651,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 +667,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 diff --git a/template/qtgui/nsmsingleserver.py b/template/qtgui/nsmsingleserver.py index f68547c..0ae3632 100644 --- a/template/qtgui/nsmsingleserver.py +++ b/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 . """ -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) diff --git a/template/qtgui/submenus.py b/template/qtgui/submenus.py index 8469864..70fb1f5 100644 --- a/template/qtgui/submenus.py +++ b/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 . """ +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 diff --git a/template/qtgui/usermanual.py b/template/qtgui/usermanual.py index c7ef498..ba821ca 100644 --- a/template/qtgui/usermanual.py +++ b/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 . """ -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 diff --git a/template/start.py b/template/start.py index 9d6f453..e23656e 100644 --- a/template/start.py +++ b/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)