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.

369 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 ),
4 years ago
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.
"""
4 years ago
#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 Argodejo/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
4 years ago
from PyQt5 import QtGui
4 years ago
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:
4 years ago
compiledVersion = False
logger.info("Compiled version: {}".format(compiledVersion))
cboxSharedObjectVersionedName = "lib"+METADATA["shortName"]+".so." + METADATA["version"]
4 years ago
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
4 years ago
}
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)
4 years ago
#Local version has higher priority
4 years ago
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
4 years ago
else:
pass
#no support for system-wide cbox in compiled mode. Error handling at the bottom of the file
4 years ago
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"),
4 years ago
#"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.
4 years ago
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")))
4 years ago
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))
4 years ago
#Construct QAppliction before constantsAndCOnfigs, which has the fontDB
#QtGui.QGuiApplication.setDesktopSettingsAware(False) #This will crash with new Qt!
qtApp = QApplication(sys.argv)
4 years ago
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.
4 years ago
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
"""
4 years ago
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())
4 years ago
libpthread_path = ctypes.util.find_library("pthread")
if not libpthread_path:
return
4 years ago
4 years ago
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
4 years ago
4 years ago
_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.
4 years ago
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
4 years ago
#path = "/tmp"
if path:
4 years ago
startPseudoNSMServer(path)
else:
sys.exit()
4 years ago
#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
4 years ago
sys._excepthook = sys.excepthook
def exception_hook(exctype, value, traceback):
"""This hook purely exists to call sys.exit(1) even on a Qt crash
4 years ago
so that atexit gets triggered"""
#print(exctype, value, traceback)
logger.error("Caught crash in execpthook. Trying too execute atexit anyway")
4 years ago
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"])
4 years ago
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:
4 years ago
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
4 years ago
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")
4 years ago
print ("sys.path start and tail:", sys.path[0:5], sys.path[-1])
exitWithMessage("Calfbox module could not be loaded")
4 years ago
#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)