Music production session manager https://www.laborejo.org
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.

251 lines
10 KiB

4 years ago
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
2 years ago
Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net )
4 years ago
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
4 years ago
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()
4 years ago
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("-l", "--load-session", action='store', dest="session", help="Session to open on startup, must exist. Overrides --continue")
parser.add_argument("-c", "--continue", action='store_true', dest="continueLastSession", help="Autostart last active session.")
parser.add_argument("-i", "--hide", action='store_true', dest="starthidden", help="Start GUI hidden in tray, only if tray available on system.")
parser.add_argument("--session-root", action='store', dest="sessionRoot", help="Root directory of all sessions. Defaults to $XDG_DATA_HOME/nsm/")
4 years ago
args = parser.parse_args()
import logging
if args.verbose: #development
logging.basicConfig(level=logging.INFO, format='[' + METADATA["shortName"] + '] %(levelname)s %(asctime)s %(name)s: %(message)s',)
4 years ago
#logging.getLogger().setLevel(logging.INFO) #development
else: #production
logging.basicConfig(level=logging.ERROR, format='[' + METADATA["shortName"] + '] %(levelname)s %(asctime)s %(name)s: %(message)s',)
4 years ago
#logging.getLogger().setLevel(logging.ERROR) #production
4 years ago
logger = logging.getLogger(__name__)
logger.info("import")
"""set up python search path before the program starts
4 years ago
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
logger.info(f"Python Version {sys.version}")
4 years ago
try:
from compiledprefix import prefix
compiledVersion = True
logger.info("Compiled prefix found: {}".format(prefix))
except ModuleNotFoundError as e:
compiledVersion = False
4 years ago
logger.info("Compiled version: {}".format(compiledVersion))
if compiledVersion:
4 years ago
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,
4 years ago
"url": args.url,
"startupSession": args.session,
"startHidden": args.starthidden, #bool
"continueLastSession": args.continueLastSession, #bool
}
4 years ago
_root = os.path.dirname(__file__)
_root = os.path.abspath(os.path.join(_root, ".."))
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"),
"sessionRoot": args.sessionRoot,
4 years ago
"url": args.url,
"startupSession": args.session,
"startHidden": args.starthidden, #bool
"continueLastSession": args.continueLastSession, #bool
}
4 years ago
if PATHS["startupSession"]:
logger.warning("--continue ignored because --load-session was used.")
PATHS["continueLastSession"] = None #just in case. See --help string
4 years ago
logger.info("PATHS: {}".format(PATHS))
#Construct QAppliction before constantsAndCOnfigs, which has the fontDB
#QApplication.setDesktopSettingsAware(False) #We need our own font so the user interface stays predictable
QApplication.setDesktopFileName(PATHS["desktopfile"])
qtApp = QApplication(sys.argv)
setPaletteAndFont(qtApp)
QApplication.setStyle(QStyleFactory.create("Fusion"))
setPaletteAndFont(qtApp)
4 years ago
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)
4 years ago
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
4 years ago
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())
4 years ago
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"])
try:
#Only cosmetics
setProcessName(METADATA["shortName"])
except:
pass
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)
4 years ago
#Catch Exceptions even if PyQt crashes.
import sys
sys._excepthook = sys.excepthook
4 years ago
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"""
4 years ago
#print(exctype, value, traceback)
logger.error("Caught crash in execpthook. Trying too execute atexit anyway")
sys._excepthook(exctype, value, traceback)
sys.exit(1)
4 years ago
sys.excepthook = exception_hook