annotate 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
rev   line source
Chris@92 1 /* -*- c-basic-offset: 8 indent-tabs-mode: t -*- */
fazekasgy@37 2 /*
fazekasgy@37 3
fazekasgy@37 4 * Vampy : This plugin is a wrapper around the Vamp plugin API.
fazekasgy@37 5 * It allows for writing Vamp plugins in Python.
fazekasgy@37 6
fazekasgy@37 7 * Centre for Digital Music, Queen Mary University of London.
fazekasgy@37 8 * Copyright (C) 2008-2009 Gyorgy Fazekas, QMUL. (See Vamp sources
fazekasgy@37 9 * for licence information.)
fazekasgy@37 10
fazekasgy@37 11 */
fazekasgy@37 12
fazekasgy@37 13
fazekasgy@37 14 #include "PyPlugScanner.h"
Chris@92 15 #include "PyExtensionManager.h"
Chris@92 16 #include "Debug.h"
fazekasgy@37 17 #include <algorithm>
fazekasgy@37 18 #include <cstdlib>
fazekasgy@37 19 //#include "vamp-hostsdk/PluginHostAdapter.h"
fazekasgy@37 20
fazekasgy@37 21 #ifdef _WIN32
fazekasgy@37 22 #include <windows.h>
fazekasgy@37 23 #include <tchar.h>
fazekasgy@37 24 #define pathsep ("\\")
fazekasgy@37 25 #else
fazekasgy@37 26 #include <dirent.h>
fazekasgy@37 27 #include <dlfcn.h>
fazekasgy@37 28 #define pathsep ("/")
fazekasgy@37 29 #endif
fazekasgy@37 30 #define joinPath(a,b) ( (a)+pathsep+(b) )
fazekasgy@37 31
fazekasgy@37 32 using std::string;
fazekasgy@37 33 using std::vector;
fazekasgy@37 34 using std::cerr;
fazekasgy@37 35 using std::endl;
fazekasgy@37 36 using std::find;
fazekasgy@37 37
fazekasgy@37 38 PyPlugScanner::PyPlugScanner()
fazekasgy@37 39 {
fazekasgy@37 40
fazekasgy@37 41 }
fazekasgy@37 42
fazekasgy@37 43 PyPlugScanner *PyPlugScanner::m_instance = NULL;
fazekasgy@37 44 bool PyPlugScanner::m_hasInstance = false;
fazekasgy@37 45
fazekasgy@37 46 PyPlugScanner*
fazekasgy@37 47 PyPlugScanner::getInstance()
fazekasgy@37 48 {
fazekasgy@37 49 if (!m_hasInstance) {
fazekasgy@37 50 m_instance = new PyPlugScanner();
fazekasgy@37 51 m_hasInstance = true;
fazekasgy@37 52 }
fazekasgy@37 53 return m_instance;
fazekasgy@37 54 }
fazekasgy@37 55
fazekasgy@37 56 void
fazekasgy@37 57 PyPlugScanner::setPath(vector<string> path)
fazekasgy@37 58 {
fazekasgy@37 59 m_path=path;
fazekasgy@37 60 }
fazekasgy@37 61
fazekasgy@37 62 // We assume that each script on the path has one valid class
fazekasgy@37 63 vector<string>
fazekasgy@37 64 PyPlugScanner::getPyPlugs()
fazekasgy@37 65 {
fazekasgy@37 66 //for_each m_path listFiles then return vector<pyPlugs>
fazekasgy@37 67 //key format: FullPathString/FileName.py:ClassName
fazekasgy@37 68
fazekasgy@37 69 bool getCompiled = true;
fazekasgy@37 70 char* getPyc = getenv("VAMPY_COMPILED");
fazekasgy@37 71 if (getPyc) {
fazekasgy@37 72 string value(getPyc);
fazekasgy@37 73 cerr << "VAMPY_COMPILED=" << value << endl;
fazekasgy@37 74 getCompiled = value.compare("1")?false:true;
fazekasgy@37 75 }
fazekasgy@37 76
fazekasgy@37 77 vector<string> pyPlugs;
fazekasgy@37 78 string pluginKey;
fazekasgy@37 79 PyObject *pyClass;
fazekasgy@37 80
fazekasgy@37 81 for (size_t i = 0; i < m_path.size(); ++i) {
fazekasgy@37 82
fazekasgy@37 83 vector<string> files = listFiles(m_path[i],"py");
fazekasgy@37 84
fazekasgy@37 85 /// recognise byte compiled plugins
fazekasgy@37 86 if (getCompiled) {
fazekasgy@58 87 vector<string> pyc_files = listFiles(m_path[i],"pyc");
fazekasgy@58 88 vector<string> pyo_files = listFiles(m_path[i],"pyo");
fazekasgy@58 89 mergeFileLists(pyc_files,pyo_files,".pyo");
fazekasgy@58 90 mergeFileLists(pyo_files,files,".py");
fazekasgy@37 91 }
fazekasgy@37 92
fazekasgy@37 93 for (vector<string>::iterator fi = files.begin();
fazekasgy@37 94 fi != files.end(); ++fi) {
fazekasgy@37 95 string script = *fi;
fazekasgy@37 96 if (!script.empty()) {
fazekasgy@37 97 string classname=script.substr(0,script.rfind('.'));
fazekasgy@37 98 pluginKey=joinPath(m_path[i],script)+":"+classname;
fazekasgy@37 99 pyClass = getScriptClass(m_path[i],classname);
fazekasgy@37 100 if (pyClass == NULL)
Chris@92 101 cerr << "Warning: Syntax error or other problem in scanning VamPy plugin: "
fazekasgy@37 102 << classname << ". Avoiding plugin." << endl;
fazekasgy@37 103 else {
fazekasgy@37 104 pyPlugs.push_back(pluginKey);
fazekasgy@37 105 m_pyClasses.push_back(pyClass);
Chris@92 106 }
fazekasgy@37 107 }
fazekasgy@37 108 }
fazekasgy@37 109 }
fazekasgy@37 110
fazekasgy@37 111 return pyPlugs;
fazekasgy@37 112
fazekasgy@37 113 }
fazekasgy@37 114
fazekasgy@37 115 /// insert python byte code names (.pyc) if a .py file can not be found
fazekasgy@37 116 /// The interpreter automatically generates byte code files and executes
fazekasgy@37 117 /// them if they exist. Therefore, we prefer .py files, but we allow
fazekasgy@37 118 /// (relatively) closed source distributions by recognising .pyc files.
fazekasgy@37 119 void
fazekasgy@58 120 PyPlugScanner::mergeFileLists(vector<string> &src, vector<string> &tg, string target_ext)
fazekasgy@37 121 {
fazekasgy@58 122 for (vector<string>::iterator srcit = src.begin();
fazekasgy@58 123 srcit != src.end(); ++srcit) {
fazekasgy@58 124 // cerr << *srcit;
fazekasgy@58 125 string src_name = *srcit;
fazekasgy@58 126 string tg_name = src_name.substr(0,src_name.rfind('.')) + target_ext;
fazekasgy@58 127 vector<string>::iterator tgit = find (tg.begin(), tg.end(), tg_name);
fazekasgy@58 128 if (tgit == tg.end()) tg.push_back(src_name);
fazekasgy@37 129 }
fazekasgy@37 130
fazekasgy@37 131 }
fazekasgy@37 132
fazekasgy@37 133
fazekasgy@37 134 //For now return one class object found in each script
fazekasgy@37 135 vector<PyObject*>
fazekasgy@37 136 PyPlugScanner::getPyClasses()
fazekasgy@37 137 {
fazekasgy@37 138 return m_pyClasses;
fazekasgy@37 139
fazekasgy@37 140 }
fazekasgy@37 141
fazekasgy@37 142 //Validate
fazekasgy@37 143 //This should not be called more than once!
fazekasgy@37 144 PyObject*
fazekasgy@37 145 PyPlugScanner::getScriptClass(string path, string classname)
fazekasgy@37 146 {
fazekasgy@37 147 //Add plugin path to active Python Path
fazekasgy@37 148 string pyCmd = "import sys\nsys.path.append('" + path + "')\n";
fazekasgy@37 149 PyRun_SimpleString(pyCmd.c_str());
fazekasgy@37 150
fazekasgy@37 151 //Assign an object to the source code
fazekasgy@37 152 PyObject *pySource = PyString_FromString(classname.c_str());
fazekasgy@37 153
fazekasgy@37 154 //Import it as a module into the py interpreter
fazekasgy@37 155 PyObject *pyModule = PyImport_Import(pySource);
fazekasgy@37 156 PyObject* pyError = PyErr_Occurred();
Chris@70 157 if (pyError) {
fazekasgy@37 158 cerr << "ERROR: error importing source: " << classname << endl;
fazekasgy@37 159 PyErr_Print();
fazekasgy@37 160 Py_DECREF(pySource);
fazekasgy@37 161 Py_CLEAR(pyModule); // safer if pyModule==NULL
fazekasgy@37 162 return NULL;
fazekasgy@37 163 }
fazekasgy@37 164 Py_DECREF(pySource);
fazekasgy@37 165
fazekasgy@37 166 //Read the dictionary object holding the namespace of the module (borrowed reference)
fazekasgy@37 167 PyObject *pyDict = PyModule_GetDict(pyModule);
fazekasgy@37 168 Py_DECREF(pyModule);
fazekasgy@37 169
fazekasgy@37 170 //Get the PluginClass from the module (borrowed reference)
fazekasgy@37 171 PyObject *pyClass = PyDict_GetItemString(pyDict, classname.c_str());
fazekasgy@37 172
Chris@92 173 if (pyClass == Py_None) {
Chris@92 174 DSTREAM << "Vampy: class name " << classname
Chris@92 175 << " is None in module; assuming it was scrubbed "
Chris@92 176 << "following an earlier load failure" << endl;
Chris@92 177 return NULL;
Chris@92 178 }
Chris@92 179
Chris@92 180 // Check if class is present and a callable method is implemented
Chris@92 181 if (!pyClass || !PyCallable_Check(pyClass)) {
fazekasgy@37 182 cerr << "ERROR: callable plugin class could not be found in source: " << classname << endl
fazekasgy@37 183 << "Hint: plugin source filename and plugin class name must be the same." << endl;
fazekasgy@37 184 PyErr_Print();
fazekasgy@37 185 return NULL;
fazekasgy@37 186 }
Chris@92 187
Chris@92 188 bool acceptable = true;
Chris@92 189
Chris@92 190 // Check that the module doesn't have any name collisions with
Chris@92 191 // our own symbols
Chris@92 192
Chris@92 193 int i = 0;
Chris@92 194 while (PyExtensionManager::m_exposedNames[i]) {
Chris@92 195
Chris@92 196 const char* name = PyExtensionManager::m_exposedNames[i];
Chris@92 197 i++;
Chris@92 198
Chris@92 199 PyObject *item = PyDict_GetItemString(pyDict, name);
Chris@92 200 if (!item) continue;
Chris@92 201
Chris@92 202 if (item == Py_None) {
Chris@92 203 DSTREAM << "Vampy: name " << name << " is None "
Chris@92 204 << "in module " << classname
Chris@92 205 << "; assuming it was cleared on unload"
Chris@92 206 << endl;
Chris@92 207 continue;
Chris@92 208 }
Chris@92 209
Chris@92 210 PyTypeObject *metatype = Py_TYPE(item);
Chris@92 211
Chris@92 212 if (!strcmp(name, "frame2RealTime")) {
Chris@92 213 if (metatype != &PyCFunction_Type) {
Chris@92 214 cerr << "ERROR: plugin " << classname
Chris@92 215 << " redefines Vampy function name \""
Chris@92 216 << name << "\" (metatype is \""
Chris@92 217 << metatype->tp_name << "\")" << endl;
Chris@92 218 acceptable = false;
Chris@92 219 break;
Chris@92 220 } else {
Chris@92 221 continue;
Chris@92 222 }
Chris@92 223 }
Chris@92 224
Chris@92 225 if (metatype != &PyType_Type) {
Chris@92 226 cerr << "ERROR: plugin " << classname
Chris@92 227 << " uses Vampy reserved type name \"" << name
Chris@92 228 << "\" for non-type (metatype is \""
Chris@92 229 << metatype->tp_name << "\")" << endl;
Chris@92 230 acceptable = false;
Chris@92 231 break;
Chris@92 232 }
Chris@92 233
Chris@92 234 PyTypeObject *type = (PyTypeObject *)item;
Chris@92 235 if (type->tp_name == std::string("vampy.") + name) {
Chris@92 236 DSTREAM << "Vampy: acceptable Vampy type name "
Chris@92 237 << type->tp_name << " found in module" << endl;
Chris@92 238 } else {
Chris@92 239 cerr << "ERROR: plugin " << classname
Chris@92 240 << " redefines Vampy type \"" << name << "\"";
Chris@92 241 if (strcmp(type->tp_name, name)) {
Chris@92 242 cerr << " (as \"" << type->tp_name << "\")";
Chris@92 243 }
Chris@92 244 cerr << endl;
Chris@92 245 acceptable = false;
Chris@92 246 break;
Chris@92 247 }
Chris@92 248 }
Chris@92 249
Chris@92 250 if (acceptable) {
Chris@92 251 return pyClass;
Chris@92 252 } else {
Chris@92 253 PyObject *key = PyString_FromString(classname.c_str());
Chris@92 254 PyDict_SetItem(pyDict, key, Py_None);
Chris@92 255 Py_DECREF(key);
Chris@92 256 return NULL;
Chris@92 257 }
fazekasgy@37 258 }
fazekasgy@37 259
fazekasgy@37 260
fazekasgy@37 261
fazekasgy@37 262 // Return a list of files in dir with given extension
fazekasgy@37 263 // Code taken from hostext/PluginLoader.cpp
fazekasgy@37 264 vector<string>
fazekasgy@37 265 PyPlugScanner::listFiles(string dir, string extension)
fazekasgy@37 266 {
fazekasgy@37 267 vector<string> files;
fazekasgy@37 268
fazekasgy@37 269 #ifdef _WIN32
fazekasgy@37 270
fazekasgy@37 271 string expression = dir + "\\*." + extension;
fazekasgy@37 272 WIN32_FIND_DATA data;
fazekasgy@37 273 HANDLE fh = FindFirstFile(expression.c_str(), &data);
fazekasgy@37 274 if (fh == INVALID_HANDLE_VALUE) return files;
fazekasgy@37 275
fazekasgy@37 276 bool ok = true;
fazekasgy@37 277 while (ok) {
fazekasgy@37 278 files.push_back(data.cFileName);
fazekasgy@37 279 ok = FindNextFile(fh, &data);
fazekasgy@37 280 }
fazekasgy@37 281
fazekasgy@37 282 FindClose(fh);
fazekasgy@37 283
fazekasgy@37 284 #else
fazekasgy@37 285
fazekasgy@37 286 size_t extlen = extension.length();
fazekasgy@37 287 DIR *d = opendir(dir.c_str());
fazekasgy@37 288 if (!d) return files;
fazekasgy@37 289
fazekasgy@37 290 struct dirent *e = 0;
fazekasgy@37 291 while ((e = readdir(d))) {
fazekasgy@37 292 size_t len = strlen(e->d_name);
fazekasgy@37 293 if (len < extlen + 2 ||
fazekasgy@37 294 e->d_name + len - extlen - 1 != "." + extension) {
fazekasgy@37 295 continue;
fazekasgy@37 296 }
fazekasgy@37 297 files.push_back(e->d_name);
fazekasgy@37 298 }
fazekasgy@37 299
fazekasgy@37 300 closedir(d);
fazekasgy@37 301 #endif
fazekasgy@37 302
fazekasgy@37 303 return files;
fazekasgy@37 304 }
fazekasgy@37 305
fazekasgy@37 306
fazekasgy@37 307 //!!! It would probably be better to actually call
fazekasgy@37 308 // PluginHostAdapter::getPluginPath. That would mean this "plugin"
fazekasgy@37 309 // needs to link against vamp-hostsdk, but that's probably acceptable
fazekasgy@37 310 // as it is sort of a host as well.
fazekasgy@37 311
fazekasgy@37 312 // std::vector<std::string>
fazekasgy@37 313 // PyPlugScanner::getAllValidPath()
fazekasgy@37 314 // {
fazekasgy@37 315 // Vamp::PluginHostAdapter host_adapter( ??? );
fazekasgy@37 316 // return host_adapter.getPluginPath();
fazekasgy@37 317 // }
fazekasgy@37 318
fazekasgy@37 319 // tried to implement it, but found a bit confusing how to
fazekasgy@37 320 // instantiate the host adapter here...
fazekasgy@37 321
fazekasgy@37 322
fazekasgy@37 323 //Return correct plugin directories as per platform
fazekasgy@37 324 //Code taken from vamp-sdk/PluginHostAdapter.cpp
fazekasgy@37 325 std::vector<std::string>
fazekasgy@37 326 PyPlugScanner::getAllValidPath()
fazekasgy@37 327 {
fazekasgy@37 328
fazekasgy@37 329 std::vector<std::string> path;
fazekasgy@37 330 std::string envPath;
fazekasgy@37 331
fazekasgy@37 332 char *cpath = getenv("VAMP_PATH");
fazekasgy@37 333 if (cpath) envPath = cpath;
fazekasgy@37 334
fazekasgy@37 335 #ifdef _WIN32
fazekasgy@37 336 #define PATH_SEPARATOR ';'
fazekasgy@37 337 #define DEFAULT_VAMP_PATH "%ProgramFiles%\\Vamp Plugins"
fazekasgy@37 338 #else
fazekasgy@37 339 #define PATH_SEPARATOR ':'
fazekasgy@37 340 #ifdef __APPLE__
fazekasgy@37 341 #define DEFAULT_VAMP_PATH "$HOME/Library/Audio/Plug-Ins/Vamp:/Library/Audio/Plug-Ins/Vamp"
fazekasgy@37 342 #else
fazekasgy@37 343 #define DEFAULT_VAMP_PATH "$HOME/vamp:$HOME/.vamp:/usr/local/lib/vamp:/usr/lib/vamp"
fazekasgy@37 344 #endif
fazekasgy@37 345 #endif
fazekasgy@37 346
fazekasgy@37 347 if (envPath == "") {
fazekasgy@37 348 envPath = DEFAULT_VAMP_PATH;
fazekasgy@37 349 char *chome = getenv("HOME");
fazekasgy@37 350 if (chome) {
fazekasgy@37 351 std::string home(chome);
fazekasgy@37 352 std::string::size_type f;
fazekasgy@37 353 while ((f = envPath.find("$HOME")) != std::string::npos &&
fazekasgy@37 354 f < envPath.length()) {
fazekasgy@37 355 envPath.replace(f, 5, home);
fazekasgy@37 356 }
fazekasgy@37 357 }
fazekasgy@37 358 #ifdef _WIN32
fazekasgy@37 359 char *cpfiles = getenv("ProgramFiles");
fazekasgy@37 360 if (!cpfiles) cpfiles = "C:\\Program Files";
fazekasgy@37 361 std::string pfiles(cpfiles);
fazekasgy@37 362 std::string::size_type f;
fazekasgy@37 363 while ((f = envPath.find("%ProgramFiles%")) != std::string::npos &&
fazekasgy@37 364 f < envPath.length()) {
fazekasgy@37 365 envPath.replace(f, 14, pfiles);
fazekasgy@37 366 }
fazekasgy@37 367 #endif
fazekasgy@37 368 }
fazekasgy@37 369
fazekasgy@37 370 std::string::size_type index = 0, newindex = 0;
fazekasgy@37 371
fazekasgy@37 372 while ((newindex = envPath.find(PATH_SEPARATOR, index)) < envPath.size()) {
fazekasgy@37 373 path.push_back(envPath.substr(index, newindex - index));
fazekasgy@37 374 index = newindex + 1;
fazekasgy@37 375 }
fazekasgy@37 376
fazekasgy@37 377 path.push_back(envPath.substr(index));
fazekasgy@37 378
fazekasgy@37 379 //can add an extra path for vampy plugins
fazekasgy@37 380 char* extraPath = getenv("VAMPY_EXTPATH");
fazekasgy@37 381 if (extraPath) {
fazekasgy@37 382 string vampyPath(extraPath);
fazekasgy@37 383 cerr << "VAMPY_EXTPATH=" << vampyPath << endl;
fazekasgy@37 384 path.push_back(vampyPath);
fazekasgy@37 385 }
fazekasgy@37 386
fazekasgy@37 387 return path;
fazekasgy@37 388 }