#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2022 , Nils Hilbricht , Germany ( https : / / www . hilbricht . net )
This file is part of the Laborejo Software Suite ( https : / / www . laborejo . org ) ,
This 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 / > .
"""
import logging ; logger = logging . getLogger ( __name__ ) ; logger . info ( " import " )
import pathlib
import configparser
import os
import stat
import xdg . DesktopEntry #pyxdg https://www.freedesktop.org/wiki/Software/pyxdg/
import xdg . IconTheme #pyxdg https://www.freedesktop.org/wiki/Software/pyxdg/
from engine . start import PATHS
def nothing ( * args ) : pass
class SupportedProgramsDatabase ( object ) :
""" Find all binaries with NSM support. Resources are:
* Agordejo internal program list of known working programs .
* Internal blacklist of known redundant programs ( such as non - daw ) or nonsense entries , like Agordejo itself
* Finally , local to a users system : User whitelist for any program , user blacklist .
Those two have the highest priority .
"""
def __init__ ( self ) :
self . progressHook = nothing #prevents the initial programstart from sending meaningless messages for the cached data. Set and reverted in self.build
self . blackList = set ( ( " nsmd " , " non-daw " , " carla " , " agordejo " , " adljack " , " agordejo.bin " , " non-midi-mapper " , " non-mixer-noui " ) ) #only programs that have to do with audio and music. There is another general blacklist that speeds up discovery
self . whiteList = set ( ( " thisdoesnotexisttest " , " patroneo " , " vico " , " tembro " , " laborejo " ,
" fluajho " , " carla-rack " , " carla-patchbay " , " carla-jack-multi " , " carla-jack-single " ,
" ardour5 " , " ardour6 " , " nsm-data " , " jackpatch " , " nsm-proxy " , " ADLplug " , " ams " ,
" drumkv1_jack " , " synthv1_jack " , " samplv1_jack " , " padthv1_jack " ,
" luppp " , " non-mixer " , " non-timeline " , " non-sequencer " ,
" OPNplug " , " qmidiarp " , " qtractor " , " zynaddsubfx " , " jack_mixer " ,
" hydrogen " , " mfp " , " shuriken " , " guitarix " , " radium " ,
" ray-proxy " , " ray-jackpatch " , " amsynth " , " mamba " , " qseq66 " , " synthpod " , " tap192 " ,
) ) #shortcut list and programs not found by buildCache_grepExecutablePaths because they are just shellscripts and do not contain /nsm/server/announce.
self . userWhitelist = ( ) #added dynamically by api.systemProgramsSetWhitelist add to morePrograms. highest priority
self . userBlacklist = ( ) #added dynamically by api.systemProgramsSetBlacklist as blacklist. highest priority
self . programs = [ ] #main data structure of this file. list of dicts. guaranteed keys: agordejoExec, name, agordejoFullPath. And probably others, like description and version.
self . nsmExecutables = set ( ) #set of executables for fast membership, if a GUI wants to know if they are available. Needs to be build "manually" with self.programs. no auto-property for a list. at least we don't want to do the work.
self . unfilteredExecutables = None #in build()
#self.build() needs to be called when the program is ready, e.g. a GUI is set up and has the progressHook ready
def _isexe ( self , path ) :
""" executable by owner """
return path . is_file ( ) and stat . S_IXUSR & os . stat ( path ) [ stat . ST_MODE ] == 64
def _executableNameToFullPath ( self , exeName : str , executableSystemPaths : set ) - > pathlib . Path :
for directory in executableSystemPaths :
p = pathlib . Path ( directory , exeName )
if p . exists ( ) :
return p
else :
return None
def gatherAllNsmClients ( self ) - > list :
"""
We parse . desktop files for the nsm flag
and export our own list of dicts with various entries . see below .
"""
executableSystemPaths = set ( [ pathlib . Path ( p ) . resolve ( ) for p in os . environ [ " PATH " ] . split ( os . pathsep ) ] ) #resolve filters out symlinks, like ArchLinux's /sbin and /bin. set() makes it unique
""" Go through all dirs including subdirs """
xdgPaths = (
pathlib . Path ( " /usr/share/applications " ) ,
pathlib . Path ( " /usr/local/share/applications " ) ,
pathlib . Path ( pathlib . Path . home ( ) , " .local/share/applications " ) ,
)
self . progressHook ( " " )
result = [ ]
for basePath in xdgPaths :
try : #TODO: this whole part of the program is a mess. Confiparser and Qt in a bundle segfault too often.
self . progressHook ( f " { basePath } " )
except Exception as e :
logger . error ( e )
pass
for f in basePath . glob ( ' **/* ' ) :
if f . is_file ( ) and f . suffix == " .desktop " :
desktopEntry = xdg . DesktopEntry . DesktopEntry ( f )
"""
#Don't validate. This is over-zealous and will mark deprecation and unknown categories.
try :
desktopEntry . validate ( )
except xdg . Exceptions . ValidationError as e :
logger . error ( f " Desktop file { f } has problems: { e } " )
continue
"""
agorExec = desktopEntry . get ( " X-NSM-Exec " ) #If there is a specific executable to start from nsm, use this. If not we use the normal executable below.
if not agorExec :
n = pathlib . Path ( desktopEntry . getExec ( ) ) . name
agorExec = n . split ( " " ) [ 0 ] . strip ( ) # this will fail with special filenames, such as spaces in filenames. But it is already the fallback for programs not adhering to the nsm specs. not our problem anymore.
blacklisted = agorExec in self . blackList or agorExec in self . userBlacklist
if blacklisted :
logger . info ( f " { agorExec } ] is blacklisted. Skip. " )
continue
isNSM = ( bool ( desktopEntry . get ( " X-NSM-Capable " ) )
or bool ( desktopEntry . get ( " X-NSM-capable " ) )
or agorExec in self . whiteList
or agorExec in self . userWhitelist
)
if isNSM :
absExecPath = self . _executableNameToFullPath ( agorExec , executableSystemPaths )
if absExecPath is None :
logger . warning ( f " Couldn ' t find actual path for { agorExec } eventhough we searched with the name from it ' s desktop file. If this program is not installed at all this is a false-negative error. Don ' t worry. " )
continue
if not self . _isexe ( absExecPath ) :
logger . error ( f " { absExecPath } was derived from .desktop file and exist, but it not executable! " )
continue
data = {
" agordejoName " : desktopEntry . getName ( ) ,
" agordejoExec " : agorExec , #to prevent 'carla-rack %u'. This is what nsm will call.
" agordejoIconPath " : xdg . IconTheme . getIconPath ( desktopEntry . getIcon ( ) ) ,
" agordejoFullPath " : absExecPath , #This is only for information. nsm calls agordejoExec
" agordejoDescription " : desktopEntry . getComment ( ) ,
}
result . append ( data )
self . progressHook ( " " )
return result
def build ( self , progressHook = None ) :
""" Can be called at any time by the user to update after installing new programs """
if progressHook :
#receives one string which indicates what files is currently parsed.
#Just the pure path, this will not get translated!
#The purpose is to show a "we are not frozen!" feedback to the user.
#It doesn't really matter what is reported back as long as it changes often
self . progressHook = progressHook
logger . info ( " Building launcher database. This might take a minute " )
self . progressHook ( " " )
self . programs = self . gatherAllNsmClients ( )
self . progressHook ( " " )
self . unfilteredExecutables = self . buildCache_unfilteredExecutables ( )
self . nsmExecutables = set ( d [ " agordejoExec " ] for d in self . programs )
self . progressHook ( " " )
self . progressHook = nothing
logger . info ( " Building launcher database done. " )
def getNsmClients ( self ) - > list :
""" Return the main data structure of this file:
a list of dicts
"""
return self . programs
def buildCache_unfilteredExecutables ( self ) :
""" Just a list of all exectuables of this systems PATH.
This is used for the GUIs " start any program " with auto completion .
"""
result = [ ]
executablePaths = [ pathlib . Path ( p ) for p in os . environ [ " PATH " ] . split ( os . pathsep ) ]
for path in executablePaths :
self . progressHook ( f " { path } " )
result + = [ str ( pathlib . Path ( f ) . relative_to ( path ) ) for f in path . glob ( " * " ) if self . _isexe ( f ) ]
return sorted ( list ( set ( result ) ) )
programDatabase = SupportedProgramsDatabase ( )