#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2020, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), This application is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ #This is the first file in the program to be actually executed, after the executable which uses this as first instruction. """ We use a 'wrong' scheme of importing modules here because there are multiple exit conditions, good and bad. We don't want to use all the libraries, including the big Qt one, only to end up displaying the --version and exit. Same with the tests if jack or nsm are running. """ from engine.config import * #includes METADATA only. No other environmental setup is executed. from qtgui.helper import setPaletteAndFont #our error boxes shall look like the rest of the program """ Check parameters first. It is possible that we will just --help or --version and exit. In this case nothing gets loaded. """ import os.path import argparse parser = argparse.ArgumentParser(description=f"""{METADATA["name"]} - Version {METADATA["version"]} - Copyright {METADATA["year"]} by {METADATA["author"]} - {METADATA["url"]}""") parser.add_argument("-v", "--version", action='version', version="{} {}".format(METADATA["name"], METADATA["version"])) parser.add_argument("-V", "--verbose", action='store_true', help="(Development) Switch the logger to INFO and give out all kinds of information to get a high-level idea of what the program is doing.") parser.add_argument("-u", "--url", action='store', dest="url", help="Force URL for the session. If there is already a running session we will connect to it. Otherwise we will start one there. Default is local host with random port. Example: osc.udp://myhost.localdomain:14294/") parser.add_argument("--nsm-url", action='store', dest="url", help="Same as --url.") parser.add_argument("-s", "--session", action='store', dest="session", help="Session to open on startup.") parser.add_argument("--session-root", action='store', dest="sessionRoot", help="Root directory of all sessions. Defaults to '$HOME/NSM Sessions'") args = parser.parse_args() import logging if args.verbose: logging.basicConfig(level=logging.INFO) #development #logging.getLogger().setLevel(logging.INFO) #development else: logging.basicConfig(level=logging.ERROR) #production #logging.getLogger().setLevel(logging.ERROR) #production logger = logging.getLogger(__name__) logger.info("import") """set up python search path before the program starts and cbox gets imported. We need to be earliest, so let's put it here. This is influence during compiling by creating a temporary file "compiledprefix.py". Nuitka complies that in, when make is finished we delete it. #Default mode is a self-contained directory relative to the uncompiled patroneo python start script """ import sys import os import os.path logger.info(f"Python Version {sys.version}") try: from compiledprefix import prefix compiledVersion = True logger.info("Compiled prefix found: {}".format(prefix)) except ModuleNotFoundError as e: compiledVersion = False logger.info("Compiled version: {}".format(compiledVersion)) cboxSharedObjectVersionedName = "lib"+METADATA["shortName"]+".so." + METADATA["version"] if compiledVersion: PATHS={ #this gets imported "root": "", "bin": os.path.join(prefix, "bin"), "doc": os.path.join(prefix, "share", "doc", METADATA["shortName"]), "desktopfile": os.path.join(prefix, "share", "applications", METADATA["shortName"] + ".desktop"), #not ~/Desktop but our desktop file "share": os.path.join(prefix, "share", METADATA["shortName"]), "templateShare": os.path.join(prefix, "share", METADATA["shortName"], "template"), "sessionRoot": args.sessionRoot, "url": args.url, "startupSession": args.session, #"lib": os.path.join(prefix, "lib", METADATA["shortName"]), #cbox is found via the PYTHONPATH } cboxSharedObjectPath = os.path.join(prefix, "lib", METADATA["shortName"], cboxSharedObjectVersionedName) _root = os.path.dirname(__file__) _root = os.path.abspath(os.path.join(_root, "..")) fallback_cboxSharedObjectPath = os.path.join(_root, "site-packages", cboxSharedObjectVersionedName) #Local version has higher priority if os.path.exists(fallback_cboxSharedObjectPath): #we are not yet installed, look in the source site-packages dir os.environ["CALFBOXLIBABSPATH"] = fallback_cboxSharedObjectPath elif os.path.exists(cboxSharedObjectPath): #we are installed os.environ["CALFBOXLIBABSPATH"] = cboxSharedObjectPath else: pass #no support for system-wide cbox in compiled mode. Error handling at the bottom of the file else: _root = os.path.dirname(__file__) _root = os.path.abspath(os.path.join(_root, "..")) PATHS={ #this gets imported "root": _root, "bin": _root, "doc": os.path.join(_root, "documentation", "out"), "desktopfile": os.path.join(_root, "desktop", "desktop.desktop"), #not ~/Desktop but our desktop file "share": os.path.join(_root, "engine", "resources"), "templateShare": os.path.join(_root, "template", "engine", "resources"), "sessionRoot": args.sessionRoot, "url": args.url, "startupSession": args.session, #"lib": "", #use only system paths } if os.path.exists (os.path.join(_root, "site-packages", cboxSharedObjectVersionedName)): os.environ["CALFBOXLIBABSPATH"] = os.path.join(_root, "site-packages", cboxSharedObjectVersionedName) #else use system-wide. 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 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. logger.info("PATHS: {}".format(PATHS)) def exitWithMessage(message:str): title = f"""{METADATA["name"]} Error""" if sys.stdout.isatty(): sys.exit(title + ": " + message) else: from PyQt5.QtWidgets import QMessageBox, QApplication #This is the start file for the Qt client so we know at least that Qt is installed and use that for a warning. qErrorApp = QApplication(sys.argv) setPaletteAndFont(qErrorApp) QMessageBox.critical(qErrorApp.desktop(), title, message) qErrorApp.quit() sys.exit(title + ": " + message) def setProcessName(executableName): """From https://stackoverflow.com/questions/31132342/change-process-name-while-executing-a-python-script """ import ctypes lib = ctypes.cdll.LoadLibrary(None) prctl = lib.prctl prctl.restype = ctypes.c_int prctl.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_ulong] def set_proctitle(new_title): result = prctl(15, new_title, 0, 0, 0) if result != 0: raise OSError("prctl result: %d" % result) set_proctitle(executableName.encode()) def _is_jack_running(): """Check for JACK""" import ctypes import os silent = os.open(os.devnull, os.O_WRONLY) stdout = os.dup(1) stderr = os.dup(2) os.dup2(silent, 1) #stdout os.dup2(silent, 2) #stderr cjack = ctypes.cdll.LoadLibrary("libjack.so.0") class jack_client_t(ctypes.Structure): _fields_ = [] cjack.jack_client_open.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.POINTER(ctypes.c_int)] #the two ints are enum and pointer to enum. #http://jackaudio.org/files/docs/html/group__ClientFunctions.html#gab8b16ee616207532d0585d04a0bd1d60 cjack.jack_client_open.restype = ctypes.POINTER(jack_client_t) ctypesJackClient = cjack.jack_client_open("probe".encode("ascii"), 0x01, None) #0x01 is the bit set for do not autostart JackNoStartServer try: ret = bool(ctypesJackClient.contents) except ValueError: #NULL pointer access ret = False cjack.jack_client_close(ctypesJackClient) os.dup2(stdout, 1) #stdout os.dup2(stderr, 2) #stderr return ret def checkJackOrExit(prettyName): import sys if not _is_jack_running(): exitWithMessage("JACK Audio Connection Kit is not running. Please start it.") checkJackOrExit(METADATA["name"]) setProcessName(METADATA["shortName"]) #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