You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
368 lines
15 KiB
368 lines
15 KiB
#! /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 <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
#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.
|
|
"""
|
|
|
|
#Give at least some feedback when C libs crash.
|
|
#Will still not work for the common case that PyQt crashes and ends Python.
|
|
#But every bit helps when hunting bugs.
|
|
import faulthandler; faulthandler.enable()
|
|
|
|
|
|
from engine.config import * #includes METADATA only. No other environmental setup is executed.
|
|
from template.qtgui.chooseSessionDirectory import ChooseSessionDirectory
|
|
from template.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 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("-s", "--save", action='store', dest="directory", help="Use this directory to save. Will be created or loaded from if already present. Deactivates Agordejo/New-Session-Manager support.")
|
|
parser.add_argument("-p", "--profiler", action='store_true', help="(Development) Run the python profiler and produce a .cprof file at quit. The name will appear in your STDOUT.")
|
|
parser.add_argument("-m", "--mute", action='store_true', help="(Development) Use a fake cbox module, effectively deactivating midi and audio.")
|
|
parser.add_argument("-V", "--verbose", action='store_true', help="(Development) Switch the logger to INFO and print out all kinds of information to get a high-level idea of what the program is doing.")
|
|
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
|
|
from PyQt5.QtWidgets import QApplication, QStyleFactory
|
|
from PyQt5 import QtGui
|
|
|
|
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"),
|
|
#"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"),
|
|
#"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))
|
|
|
|
#Construct QAppliction before constantsAndCOnfigs, which has the fontDB
|
|
#QtGui.QGuiApplication.setDesktopSettingsAware(False) #This will crash with new Qt!
|
|
qtApp = QApplication(sys.argv)
|
|
setPaletteAndFont(qtApp)
|
|
QApplication.setStyle(QStyleFactory.create("Fusion"))
|
|
setPaletteAndFont(qtApp)
|
|
|
|
def exitWithMessage(message:str):
|
|
title = f"""{METADATA["name"]} Error"""
|
|
if sys.stdout.isatty():
|
|
sys.exit(title + ": " + message)
|
|
else:
|
|
from PyQt5.QtWidgets import QMessageBox
|
|
#This is the start file for the Qt client so we know at least that Qt is installed and use that for a warning.
|
|
QMessageBox.critical(qtApp.desktop(), title, message)
|
|
sys.exit(title + ": " + message)
|
|
|
|
def setProcessName(executableName):
|
|
"""From
|
|
https://stackoverflow.com/questions/31132342/change-process-name-while-executing-a-python-script
|
|
"""
|
|
import ctypes, ctypes.util
|
|
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())
|
|
|
|
|
|
libpthread_path = ctypes.util.find_library("pthread")
|
|
if not libpthread_path:
|
|
return
|
|
|
|
libpthread = ctypes.CDLL(libpthread_path)
|
|
if hasattr(libpthread, "pthread_setname_np"):
|
|
_pthread_setname_np = libpthread.pthread_setname_np
|
|
|
|
_pthread_self = libpthread.pthread_self
|
|
_pthread_self.argtypes = []
|
|
_pthread_self.restype = ctypes.c_void_p
|
|
|
|
_pthread_setname_np.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
|
_pthread_setname_np.restype = ctypes.c_int
|
|
|
|
if _pthread_setname_np is None:
|
|
return
|
|
|
|
_pthread_setname_np(_pthread_self(), executableName.encode())
|
|
|
|
def checkNsmOrExit(prettyName):
|
|
"""Check for NSM"""
|
|
#NSM changes our cwd to whereever we started new-session-manager from.
|
|
#print (os.getcwd())
|
|
import sys
|
|
from os import getenv
|
|
if not getenv("NSM_URL"): #NSMClient checks for this itself but we can anticipate an error and inform the user.
|
|
|
|
path = ChooseSessionDirectory(qtApp).path #ChooseSessionDirectory is calling exec. We can't call qtapp.exec_ because that blocks forever, even after quitting the window.
|
|
#qSessionDirApp.quit()
|
|
#del qSessionDirApp
|
|
#path = "/tmp"
|
|
|
|
if path:
|
|
startPseudoNSMServer(path)
|
|
else:
|
|
sys.exit()
|
|
|
|
#message = f"""Please start {prettyName} only through the New Session Manager (NSM) or use the --save command line parameter."""
|
|
#exitWithMessage(message)
|
|
|
|
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(mute, prettyName):
|
|
import sys
|
|
if not mute and (not _is_jack_running()):
|
|
exitWithMessage("JACK Audio Connection Kit is not running. Please start it.")
|
|
|
|
from contextlib import contextmanager
|
|
@contextmanager
|
|
def profiler(*pargs, **kwds):
|
|
"""Eventhough this is a context manager we never get past the yield statement because our
|
|
program quits the moment we receive NSM quit signal. The only chance is to register a atexit
|
|
handler which will be called no matter what"""
|
|
if args.profiler:
|
|
import tracemalloc
|
|
import cProfile
|
|
import atexit
|
|
def profilerExit(pr):
|
|
snapshot = tracemalloc.take_snapshot()
|
|
top_stats = snapshot.statistics('lineno')
|
|
print("[ Top 10 ]")
|
|
for stat in top_stats[:10]:
|
|
print(stat)
|
|
|
|
from tempfile import NamedTemporaryFile
|
|
cprofPath = NamedTemporaryFile().name + ".cprof"
|
|
pr.dump_stats(cprofPath)
|
|
logger.info("{}: write profiling data to {}".format(METADATA["name"], cprofPath))
|
|
print (f"pyprof2calltree -k -i {cprofPath}")
|
|
|
|
pr = cProfile.Profile()
|
|
pr.enable()
|
|
#Closing and file writing happens in guiMainWindow._nsmQuit
|
|
tracemalloc.start()
|
|
atexit.register(lambda: profilerExit(pr))
|
|
|
|
#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
|
|
assert not getenv("NSM_URL")
|
|
from .qtgui.nsmsingleserver import startSingleNSMServer
|
|
startSingleNSMServer(path) #provides NSM_URL environment variable and a limited drop-in replacement for NSM that will only answer to our application
|
|
assert getenv("NSM_URL")
|
|
sys.path.append("site-packages") # If you compiled but did not install you can still run with the local build of cbox in our temp dir site-packages. Add path to the last place, in case there is an installed or bundled version
|
|
|
|
if args.directory:
|
|
#Switch to the mode without NSM.
|
|
startPseudoNSMServer(args.directory)
|
|
|
|
checkNsmOrExit(METADATA["name"])
|
|
checkJackOrExit(args.mute, METADATA["name"])
|
|
try:
|
|
#Only cosmetics
|
|
setProcessName(METADATA["shortName"])
|
|
except:
|
|
pass
|
|
|
|
if args.mute:
|
|
"""This uses the fact that imports are global and only triggered once.
|
|
Inside nullbox it will overwrite the actual cbox functions,
|
|
so that the rest of the program can naively import cbox directly.
|
|
We only need this one line to change between the two cbox modes.
|
|
|
|
There are four possible states:
|
|
1) run program locally, system wide calfbox
|
|
2) run program installed, calfbox bundled/installed
|
|
both work fine with a simple import calfbox.nullbox because
|
|
bundled has already changed the cbox/python search path
|
|
|
|
3) run program locally, calfbox in local tree
|
|
Needs adding of the local tree into python search path.
|
|
|
|
4) impossible: run program installed, calfbox in local tree
|
|
|
|
"""
|
|
if not compiledVersion:
|
|
import template.calfbox.py
|
|
sys.modules["calfbox"] = sys.modules["template.calfbox.py"]
|
|
|
|
import calfbox.nullbox
|
|
|
|
|
|
#Make sure calfbox is available.
|
|
|
|
if "CALFBOXLIBABSPATH" in os.environ:
|
|
logger.info("Looking for calfbox shared library in absolute path: {}".format(os.environ["CALFBOXLIBABSPATH"]))
|
|
else:
|
|
logger.info("Looking for calfbox shared library systemwide through ctypes.util.find_library")
|
|
|
|
try:
|
|
from calfbox import cbox
|
|
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)
|
|
print ("Here is some information. Please show this to the developers.")
|
|
if "calfbox" in sys.modules:
|
|
print (sys.modules["calfbox"], "->", os.path.abspath(sys.modules["calfbox"].__file__))
|
|
else:
|
|
print ("calfbox python module is not in sys.modules. This means it truly can't be found or you forgot --mute")
|
|
|
|
print ("sys.path start and tail:", sys.path[0:5], sys.path[-1])
|
|
exitWithMessage("Calfbox module could not be loaded")
|
|
|
|
|
|
#Capture Ctlr+C / SIGINT and let @atexit handle the rest.
|
|
import signal
|
|
import sys
|
|
def signal_handler(sig, frame):
|
|
sys.exit(0) #atexit will trigger
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
|