view PyPlugScanner.cpp @ 92:a6718f9fe942

If a module appears to redefine one of our own types, refuse to load it. Also clear out the class dict for all refused modules now, so that we don't get stale names on the next scan due to not having cleared the module on unload
author Chris Cannam
date Mon, 14 Jan 2019 16:19:44 +0000
parents 0120dac53a69
children 2f2292b029a4
line wrap: on
line source
/* -*- c-basic-offset: 8 indent-tabs-mode: t -*- */
/*

 * Vampy : This plugin is a wrapper around the Vamp plugin API.
 * It allows for writing Vamp plugins in Python.

 * Centre for Digital Music, Queen Mary University of London.
 * Copyright (C) 2008-2009 Gyorgy Fazekas, QMUL. (See Vamp sources 
 * for licence information.)

*/


#include "PyPlugScanner.h"
#include "PyExtensionManager.h"
#include "Debug.h"
#include <algorithm>
#include <cstdlib>
//#include "vamp-hostsdk/PluginHostAdapter.h"

#ifdef _WIN32
#include <windows.h>
#include <tchar.h>
#define pathsep ("\\")
#else 
#include <dirent.h>
#include <dlfcn.h>
#define pathsep ("/")
#endif 
#define joinPath(a,b) ( (a)+pathsep+(b) )

using std::string;
using std::vector;
using std::cerr;
using std::endl;
using std::find;

PyPlugScanner::PyPlugScanner()
{

} 

PyPlugScanner *PyPlugScanner::m_instance = NULL;
bool PyPlugScanner::m_hasInstance = false;

PyPlugScanner*
PyPlugScanner::getInstance()
{
	if (!m_hasInstance) {
		m_instance = new PyPlugScanner();
		m_hasInstance = true;
	}
	return m_instance;
}

void
PyPlugScanner::setPath(vector<string> path)
{
	m_path=path;
}

// We assume that each script on the path has one valid class
vector<string> 
PyPlugScanner::getPyPlugs()
{
	//for_each m_path listFiles then return vector<pyPlugs>
	//key format: FullPathString/FileName.py:ClassName
	
	bool getCompiled = true;
	char* getPyc = getenv("VAMPY_COMPILED");
	if (getPyc) {
		string value(getPyc);
		cerr << "VAMPY_COMPILED=" << value << endl;
		getCompiled = value.compare("1")?false:true;
	}
	
	vector<string> pyPlugs;
	string pluginKey;
	PyObject *pyClass;
	
    for (size_t i = 0; i < m_path.size(); ++i) {
        
        vector<string> files = listFiles(m_path[i],"py");

        /// recognise byte compiled plugins
		if (getCompiled) {
        	vector<string> pyc_files = listFiles(m_path[i],"pyc");
        	vector<string> pyo_files = listFiles(m_path[i],"pyo");
			mergeFileLists(pyc_files,pyo_files,".pyo");
			mergeFileLists(pyo_files,files,".py");
		}

        for (vector<string>::iterator fi = files.begin();
             fi != files.end(); ++fi) {
				string script = *fi;
				if (!script.empty()) {					
					string classname=script.substr(0,script.rfind('.'));
					pluginKey=joinPath(m_path[i],script)+":"+classname;
					pyClass = getScriptClass(m_path[i],classname);
					if (pyClass == NULL) 
					cerr << "Warning: Syntax error or other problem in scanning VamPy plugin: " 
					     << classname << ". Avoiding plugin." << endl;
					else { 
							pyPlugs.push_back(pluginKey);
							m_pyClasses.push_back(pyClass);
					}
				}
		}		
	}
		
return pyPlugs;	

}

/// insert python byte code names (.pyc) if a .py file can not be found
/// The interpreter automatically generates byte code files and executes
/// them if they exist. Therefore, we prefer .py files, but we allow
/// (relatively) closed source distributions by recognising .pyc files.
void
PyPlugScanner::mergeFileLists(vector<string> &src, vector<string> &tg, string target_ext)
{
    for (vector<string>::iterator srcit = src.begin();
    	srcit != src.end(); ++srcit) {
			// cerr << *srcit;
			string src_name = *srcit;
			string tg_name = src_name.substr(0,src_name.rfind('.')) + target_ext;
			vector<string>::iterator tgit = find (tg.begin(), tg.end(), tg_name);
			if (tgit == tg.end()) tg.push_back(src_name);
	}
	
}


//For now return one class object found in each script
vector<PyObject*> 
PyPlugScanner::getPyClasses()
{
return m_pyClasses;	

}

//Validate
//This should not be called more than once!
PyObject* 
PyPlugScanner::getScriptClass(string path, string classname)
{
	//Add plugin path to active Python Path 
	string pyCmd = "import sys\nsys.path.append('" + path + "')\n";
	PyRun_SimpleString(pyCmd.c_str());

	//Assign an object to the source code
	PyObject *pySource = PyString_FromString(classname.c_str());

	//Import it as a module into the py interpreter
	PyObject *pyModule = PyImport_Import(pySource);
	PyObject* pyError = PyErr_Occurred();
	if (pyError) { 
		cerr << "ERROR: error importing source: " << classname << endl; 
		PyErr_Print(); 
		Py_DECREF(pySource); 
		Py_CLEAR(pyModule);  // safer if pyModule==NULL
		return NULL; 
	}
	Py_DECREF(pySource);

	//Read the dictionary object holding the namespace of the module (borrowed reference)
	PyObject *pyDict = PyModule_GetDict(pyModule);
	Py_DECREF(pyModule);

	//Get the PluginClass from the module (borrowed reference)
	PyObject *pyClass = PyDict_GetItemString(pyDict, classname.c_str());

	if (pyClass == Py_None) {
		DSTREAM << "Vampy: class name " << classname
			<< " is None in module; assuming it was scrubbed "
			<< "following an earlier load failure" << endl;
		return NULL;
	}
	
	// Check if class is present and a callable method is implemented
	if (!pyClass || !PyCallable_Check(pyClass)) {
		cerr << "ERROR: callable plugin class could not be found in source: " << classname << endl 
			<< "Hint: plugin source filename and plugin class name must be the same." << endl;
		PyErr_Print(); 
		return NULL;
	}

	bool acceptable = true;
	
        // Check that the module doesn't have any name collisions with
        // our own symbols

        int i = 0;
        while (PyExtensionManager::m_exposedNames[i]) {

		const char* name = PyExtensionManager::m_exposedNames[i];
		i++;

		PyObject *item = PyDict_GetItemString(pyDict, name);
		if (!item) continue;

		if (item == Py_None) {
			DSTREAM << "Vampy: name " << name << " is None "
				<< "in module " << classname
				<< "; assuming it was cleared on unload"
				<< endl;
			continue;
		}
		
		PyTypeObject *metatype = Py_TYPE(item);

		if (!strcmp(name, "frame2RealTime")) {
			if (metatype != &PyCFunction_Type) {
				cerr << "ERROR: plugin " << classname
				     << " redefines Vampy function name \""
				     << name << "\" (metatype is \""
				     << metatype->tp_name << "\")" << endl;
				acceptable = false;
				break;
			} else {
				continue;
			}
		}
		
		if (metatype != &PyType_Type) {
			cerr << "ERROR: plugin " << classname
			     << " uses Vampy reserved type name \"" << name
			     << "\" for non-type (metatype is \""
			     << metatype->tp_name << "\")" << endl;
			acceptable = false;
			break;
		}

		PyTypeObject *type = (PyTypeObject *)item;
		if (type->tp_name == std::string("vampy.") + name) {
			DSTREAM << "Vampy: acceptable Vampy type name "
				<< type->tp_name << " found in module" << endl;
		} else {
			cerr << "ERROR: plugin " << classname
			     << " redefines Vampy type \"" << name << "\"";
			if (strcmp(type->tp_name, name)) {
				cerr << " (as \"" << type->tp_name << "\")";
			}
			cerr << endl;
			acceptable = false;
			break;
		}
        }

	if (acceptable) {
		return pyClass;
	} else {
		PyObject *key = PyString_FromString(classname.c_str());
		PyDict_SetItem(pyDict, key, Py_None);
		Py_DECREF(key);
		return NULL;
	}
}



// Return a list of files in dir with given extension
// Code taken from hostext/PluginLoader.cpp
vector<string>
PyPlugScanner::listFiles(string dir, string extension)
{
    vector<string> files;

#ifdef _WIN32

    string expression = dir + "\\*." + extension;
    WIN32_FIND_DATA data;
    HANDLE fh = FindFirstFile(expression.c_str(), &data);
    if (fh == INVALID_HANDLE_VALUE) return files;

    bool ok = true;
    while (ok) {
        files.push_back(data.cFileName);
        ok = FindNextFile(fh, &data);
    }

    FindClose(fh);

#else

    size_t extlen = extension.length();
    DIR *d = opendir(dir.c_str());
    if (!d) return files;
            
    struct dirent *e = 0;
    while ((e = readdir(d))) {
        size_t len = strlen(e->d_name);
        if (len < extlen + 2 ||
            e->d_name + len - extlen - 1 != "." + extension) {
            continue;
        }
        files.push_back(e->d_name);
    }

    closedir(d);
#endif

    return files;
}


//!!! It would probably be better to actually call
// PluginHostAdapter::getPluginPath.  That would mean this "plugin"
// needs to link against vamp-hostsdk, but that's probably acceptable
// as it is sort of a host as well.

// std::vector<std::string>
// PyPlugScanner::getAllValidPath()
// { 
// 	Vamp::PluginHostAdapter host_adapter( ??? );
// 	return host_adapter.getPluginPath();
// }

// tried to implement it, but found a bit confusing how to 
// instantiate the host adapter here...


//Return correct plugin directories as per platform
//Code taken from vamp-sdk/PluginHostAdapter.cpp
std::vector<std::string>
PyPlugScanner::getAllValidPath()
{
	
    std::vector<std::string> path;
    std::string envPath;

    char *cpath = getenv("VAMP_PATH");
    if (cpath) envPath = cpath;

#ifdef _WIN32
#define PATH_SEPARATOR ';'
#define DEFAULT_VAMP_PATH "%ProgramFiles%\\Vamp Plugins"
#else
#define PATH_SEPARATOR ':'
#ifdef __APPLE__
#define DEFAULT_VAMP_PATH "$HOME/Library/Audio/Plug-Ins/Vamp:/Library/Audio/Plug-Ins/Vamp"
#else
#define DEFAULT_VAMP_PATH "$HOME/vamp:$HOME/.vamp:/usr/local/lib/vamp:/usr/lib/vamp"
#endif
#endif

    if (envPath == "") {
        envPath = DEFAULT_VAMP_PATH;
        char *chome = getenv("HOME");
        if (chome) {
            std::string home(chome);
            std::string::size_type f;
            while ((f = envPath.find("$HOME")) != std::string::npos &&
                    f < envPath.length()) {
                envPath.replace(f, 5, home);
            }
        }
#ifdef _WIN32
        char *cpfiles = getenv("ProgramFiles");
        if (!cpfiles) cpfiles = "C:\\Program Files";
        std::string pfiles(cpfiles);
        std::string::size_type f;
        while ((f = envPath.find("%ProgramFiles%")) != std::string::npos &&
               f < envPath.length()) {
            envPath.replace(f, 14, pfiles);
        }
#endif
    }

    std::string::size_type index = 0, newindex = 0;

    while ((newindex = envPath.find(PATH_SEPARATOR, index)) < envPath.size()) {
	path.push_back(envPath.substr(index, newindex - index));
	index = newindex + 1;
    }
    
    path.push_back(envPath.substr(index));

	//can add an extra path for vampy plugins
	char* extraPath = getenv("VAMPY_EXTPATH");
	if (extraPath) {
		string vampyPath(extraPath);
		cerr << "VAMPY_EXTPATH=" << vampyPath << endl;
		path.push_back(vampyPath);
	}
	
    return path;
}