#! /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 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 ' $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
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 } " )
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 ) )
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 ,
" startHidden " : args . starthidden , #bool
" continueLastSession " : args . continueLastSession , #bool
}
_root = os . path . dirname ( __file__ )
_root = os . path . abspath ( os . path . join ( _root , " .. " ) )
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 ,
" startHidden " : args . starthidden , #bool
" continueLastSession " : args . continueLastSession , #bool
}
if PATHS [ " startupSession " ] :
logger . warning ( " --continue ignored because --load-session was used. " )
PATHS [ " continueLastSession " ] = None #just in case. See --help string
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 )
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 _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
#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 )
#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