#! /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 . """ 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": try: #the xdg lib will still raise errors when encountering a malformed .desktop file desktopEntry = xdg.DesktopEntry.DesktopEntry(f) except Exception as e: logger.error(f"Desktop file {f} has problems: {e}") continue """ #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()