#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2020 , Nils Hilbricht , Germany ( https : / / www . hilbricht . net )
The Non - Session - Manager by Jonathan Moore Liles < male @tuxfamily . org > : http : / / non . tuxfamily . org / nsm /
With help from code fragments from https : / / github . com / attwad / python - osc ( DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE v2 )
API documentation : http : / / non . tuxfamily . org / nsm / API . html
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 / > .
"""
import logging ; logger = logging . getLogger ( __name__ ) ; logger . info ( " import " )
import pathlib
import configparser
import subprocess
import os
import stat
class SupportedProgramsDatabase ( object ) :
""" Find all binaries. Use all available resources: xdg desktop files, binary string seach in
executables , data bases , supplement with user choices .
We generate the same format as configParser does with . desktop files as _sections dict .
Example :
{ ' categories ' : ' AudioVideo;Audio;X-Recorders;X-Multitrack;X-Jack; ' ,
' comment ' : ' Easy to use pattern sequencer for JACK and NSM ' ,
' comment[de] ' : ' Einfach zu bedienender Pattern-Sequencer ' ,
' exec ' : ' patroneo ' ,
' genericname ' : ' Sequencer ' ,
' icon ' : ' patroneo ' ,
' name ' : ' Patroneo ' ,
' startupnotify ' : ' false ' ,
' terminal ' : ' false ' ,
' type ' : ' Application ' ,
' x-nsm-capable ' : ' true ' }
In case there is a file in PATH or database but has no . desktop we create our own entry with
missing data .
"""
def __init__ ( self ) :
self . blacklist = ( " nsmd " , " non-daw " , " carla " )
self . morePrograms = ( " thisdoesnotexisttest " , " carla-rack " , " carla-patchbay " , " carla-jack-multi " , " ardour5 " , " ardour6 " , " nsm-data " , " nsm-jack " ) #only programs not found by buildCache_grepExecutablePaths because they are just shellscripts and do not contain /nsm/server/announce.
self . knownDesktopFiles = { #shortcuts to the correct desktop files. Reverse lookup binary->desktop creates false entries, for example ZynAddSubFx and Carla.
" zynaddsubfx " : " zynaddsubfx-jack.desktop " , #value will later get replaced with the .desktop entry
" carla-jack-multi " : " carla.desktop " ,
}
self . programs = [ ] #list of dicts. guaranteed keys: argodejoExec, name, argodejoFullPath. 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.
#.build needs to be called from the api/GUI.
self . unfilteredExecutables = self . buildCache_unfilteredExecutables ( ) #This doesn't take too long. we can start that every time. It will get updated in build as well.
#self.build() #fills self.programs and
def buildCache_grepExecutablePaths ( self ) :
""" return a list of executable names in the path
Grep explained :
- s silent . No errors , eventhough subprocess uses stdout only
- R recursive with symlinks . We don ' t want to look in subdirs because that is not allowed by
PATH and nsm , but we want to follow symlinks
"""
result = [ ]
executablePaths = [ pathlib . Path ( p ) for p in os . environ [ " PATH " ] . split ( " : " ) ]
for path in executablePaths :
command = f " grep -iRsnl { path } -e /nsm/server/announce "
#Py>=3.7 completedProcess = subprocess.run(command, capture_output=True, text=True, shell=True)
completedProcess = subprocess . run ( command , stdout = subprocess . PIPE , stderr = subprocess . PIPE , universal_newlines = True , shell = True ) #universal_newlines is an alias for text, which was deprecated in 3.7 because text is more understandable. capture_output replaces the two PIPEs in 3.7
for fullPath in completedProcess . stdout . split ( ) :
exe = pathlib . Path ( fullPath ) . relative_to ( path )
if not str ( exe ) in self . blacklist :
result . append ( ( str ( exe ) , str ( fullPath ) ) )
for prg in self . morePrograms :
for path in executablePaths :
if pathlib . Path ( path , prg ) . is_file ( ) :
result . append ( ( str ( prg ) , str ( pathlib . Path ( path , prg ) ) ) )
break #inner loop
return result
def buildCache_DesktopEntries ( self ) :
""" 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 " ) ,
)
config = configparser . ConfigParser ( )
allDesktopEntries = [ ]
for basePath in xdgPaths :
for f in basePath . glob ( ' **/* ' ) :
if f . is_file ( ) and f . suffix == " .desktop " :
config . clear ( )
config . read ( f )
entryDict = dict ( config . _sections [ " Desktop Entry " ] )
if f . name == " zynaddsubfx-jack.desktop " :
self . knownDesktopFiles [ " zynaddsubfx " ] = entryDict
elif f . name == " carla.desktop " :
self . knownDesktopFiles [ " carla-jack-multi " ] = entryDict
#in any case:
allDesktopEntries . append ( entryDict ) #_sections 'DesktopEntry':{dictOfActualData)
return allDesktopEntries
def loadPrograms ( self , listOfDicts ) :
""" Qt Settings will send us this """
self . programs = listOfDicts
self . nsmExecutables = set ( d [ " argodejoExec " ] for d in self . programs )
def build ( self ) :
""" Can be called at any time by the user to update after installing new programs """
logger . info ( " Building launcher database. This might take a minute " )
self . programs = self . _build ( )
self . unfilteredExecutables = self . buildCache_unfilteredExecutables ( )
self . nsmExecutables = set ( d [ " argodejoExec " ] for d in self . programs )
self . _buildWhitelist ( )
logger . info ( " Building launcher database done. " )
def _exeToDesktopEntry ( self , exe : str ) - > dict :
""" Assumes self.desktopEntries is up to date
Convert one exe ( not full path ! ) to one dict entry . """
if exe in self . knownDesktopFiles : #Shortcut through internal database
entry = self . knownDesktopFiles [ exe ]
else : #Reverse Search desktop files.
for entry in self . desktopEntries :
if " exec " in entry and exe . lower ( ) in entry [ " exec " ] . lower ( ) :
return entry
#else: #Foor loop ended. Did not find any matching desktop file
return None
def _build ( self ) :
self . executables = self . buildCache_grepExecutablePaths ( )
self . desktopEntries = self . buildCache_DesktopEntries ( )
leftovers = set ( self . executables )
matches = [ ] #list of dicts
for exe , fullPath in self . executables :
entry = self . _exeToDesktopEntry ( exe )
if entry : #Found match!
entry [ " argodejoFullPath " ] = fullPath
#We don't want .desktop syntax like "qmidiarp %F"
entry [ " argodejoExec " ] = exe
matches . append ( entry )
try :
leftovers . remove ( ( exe , fullPath ) )
except KeyError :
pass #Double entries like zyn-jack zyn-alsa etc.
"""
if exe in self . knownDesktopFiles : #Shortcut through internal database
entry = self . knownDesktopFiles [ exe ]
else : #Reverse Search desktop files.
for entry in self . desktopEntries :
if " exec " in entry and exe . lower ( ) in entry [ " exec " ] . lower ( ) :
break #found entry. break inner loop to keep it
else : #Foor loop ended. Did not find any matching desktop file
#If we omit continue it will just write exe and fullPath in any desktop file.
continue
#Found match!
entry [ " argodejoFullPath " ] = fullPath
#We don't want .desktop syntax like "qmidiarp %F"
entry [ " argodejoExec " ] = exe
matches . append ( entry )
try :
leftovers . remove ( ( exe , fullPath ) )
except KeyError :
pass #Double entries like zyn-jack zyn-alsa etc.
"""
for exe , fullPath in leftovers :
pseudoEntry = { " name " : exe . title ( ) , " argodejoExec " : exe , " argodejoFullPath " : fullPath }
matches . append ( pseudoEntry )
return matches
def buildCache_unfilteredExecutables ( self ) :
def isexe ( path ) :
""" executable by owner """
return path . is_file ( ) and stat . S_IXUSR & os . stat ( path ) [ stat . ST_MODE ] == 64
result = [ ]
executablePaths = [ pathlib . Path ( p ) for p in os . environ [ " PATH " ] . split ( " : " ) ]
for path in executablePaths :
result + = [ str ( pathlib . Path ( f ) . relative_to ( path ) ) for f in path . glob ( " * " ) if isexe ( f ) ]
return sorted ( list ( set ( result ) ) )
def _buildWhitelist ( self ) :
""" For reliable, fast and easy selection this is the whitelist.
It will be populated from a template - list of well - working clients and then all binaries not
in the path are filtered out . This can be presented to the user without worries . """
#Assumes to be called only from self.build
startexecutables = set ( ( " doesnotexist " , " laborejo2 " , " patroneo " , " vico " , " fluajho " , " carla-rack " , " carla-patchbay " , " carla-jack-multi " , " ardour6 " , " drumkv1_jack " , " synthv1_jack " , " padthv1_jack " , " samplv1_jack " , " zynaddsubfx " , " ADLplug " , " OPNplug " , " non-mixer " , " non-sequencer " , " non-timeline " ) )
for prog in self . programs :
prog [ " whitelist " ] = prog [ " argodejoExec " ] in startexecutables
"""
matches = [ ]
for exe in startexecutables :
entry = self . _exeToDesktopEntry ( exe )
if entry : #Found match!
#We don't want .desktop syntax like "qmidiarp %F"
entry [ " argodejoExec " ] = exe
matches . append ( entry )
return matches
"""
programDatabase = SupportedProgramsDatabase ( )