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.
 
 

196 lines
9.6 KiB

#! /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()