#! /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 .
"""
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 Non-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
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 ) )
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 checkNsmOrExit ( prettyName ) :
""" Check for NSM """
#NSM changes our cwd to whereever we started non-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.
from PyQt5 . QtWidgets import QMessageBox , QApplication
qSessionDirApp = QApplication ( sys . argv )
setPaletteAndFont ( qSessionDirApp )
pathDialog = ChooseSessionDirectory ( qSessionDirApp )
qSessionDirApp . quit ( ) #pathDialog somehow survives
if pathDialog . path :
startPseudoNSMServer ( pathDialog . path )
else :
sys . exit ( )
#message = f"""Please start {prettyName} only through the Non 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 " ] )
setProcessName ( METADATA [ " shortName " ] )
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 " )