Chris@2: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@0: Chris@0: /** Chris@28: * [Vamp] Plugin Load Checker Chris@0: * Chris@4: * This program accepts the name of a descriptor symbol as its only Chris@1: * command-line argument. It then reads a list of plugin library paths Chris@1: * from stdin, one per line. For each path read, it attempts to load Chris@4: * that library and retrieve the named descriptor symbol, printing a Chris@1: * line to stdout reporting whether this was successful or not and Chris@1: * then flushing stdout. The output line format is described Chris@1: * below. The program exits with code 0 if all libraries were loaded Chris@1: * successfully and non-zero otherwise. Chris@0: * Chris@0: * Note that library paths must be ready to pass to dlopen() or Chris@0: * equivalent; this usually means they should be absolute paths. Chris@0: * Chris@0: * Output line for successful load of library libname.so: Chris@0: * SUCCESS|/path/to/libname.so| Chris@0: * Chris@0: * Output line for failed load of library libname.so: Chris@40: * FAILURE|/path/to/libname.so|Error message [failureCode] Chris@40: * Chris@40: * or: Chris@40: * FAILURE|/path/to/libname.so|[failureCode] Chris@40: * Chris@40: * where the error message is an optional system-level message, such Chris@40: * as may be returned from strerror or similar (which should be in the Chris@40: * native language for the system ready to show the user), and the Chris@40: * failureCode in square brackets is a mandatory number corresponding Chris@40: * to one of the PluginCandidates::FailureCode values (requiring Chris@40: * conversion to a translated string by the client). Chris@0: * Chris@28: * Although this program was written for use with Vamp audio analysis Chris@28: * plugins, it also works with other plugin formats. The program has Chris@28: * some hardcoded knowledge of Vamp, LADSPA, and DSSI plugins, but it Chris@28: * can be used with any plugins that involve loading DLLs and looking Chris@28: * up descriptor functions from them. Chris@28: * Chris@2: * Sometimes plugins will crash completely on load, bringing down this Chris@2: * program with them. If the program exits before all listed plugins Chris@2: * have been checked, this means that the plugin following the last Chris@2: * reported one has crashed. Typically the caller may want to run it Chris@2: * again, omitting that plugin. Chris@0: */ Chris@0: Chris@5: /* Chris@28: Copyright (c) 2016-2017 Queen Mary, University of London Chris@5: Chris@5: Permission is hereby granted, free of charge, to any person Chris@5: obtaining a copy of this software and associated documentation Chris@5: files (the "Software"), to deal in the Software without Chris@5: restriction, including without limitation the rights to use, copy, Chris@5: modify, merge, publish, distribute, sublicense, and/or sell copies Chris@5: of the Software, and to permit persons to whom the Software is Chris@5: furnished to do so, subject to the following conditions: Chris@5: Chris@5: The above copyright notice and this permission notice shall be Chris@5: included in all copies or substantial portions of the Software. Chris@5: Chris@5: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, Chris@5: EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF Chris@5: MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND Chris@5: NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY Chris@5: CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF Chris@5: CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION Chris@5: WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Chris@5: Chris@5: Except as contained in this notice, the names of the Centre for Chris@5: Digital Music and Queen Mary, University of London shall not be Chris@5: used in advertising or otherwise to promote the sale, use or other Chris@5: dealings in this Software without prior written authorization. Chris@5: */ Chris@5: Chris@27: #include "../version.h" Chris@27: Chris@40: #include "../checker/checkcode.h" Chris@40: Chris@28: static const char programName[] = "vamp-plugin-load-checker"; Chris@28: Chris@0: #ifdef _WIN32 Chris@0: #include Chris@0: #include Chris@49: #include Chris@49: #else Chris@49: #include Chris@38: #endif Chris@38: Chris@49: #include Chris@49: Chris@10: #include Chris@38: #include Chris@50: #include Chris@38: Chris@38: #ifdef _WIN32 Chris@38: #ifndef UNICODE Chris@38: #error "This must be compiled with UNICODE defined" Chris@38: #endif Chris@40: Chris@18: static std::string lastLibraryName = ""; Chris@40: Chris@42: static HMODULE loadLibraryUTF8(std::string name) { Chris@18: lastLibraryName = name; Chris@10: int n = name.size(); Chris@16: int wn = MultiByteToWideChar(CP_UTF8, 0, name.c_str(), n, 0, 0); Chris@16: wchar_t *wname = new wchar_t[wn+1]; Chris@16: wn = MultiByteToWideChar(CP_UTF8, 0, name.c_str(), n, wname, wn); Chris@16: wname[wn] = L'\0'; Chris@10: HMODULE h = LoadLibraryW(wname); Chris@10: delete[] wname; Chris@10: return h; Chris@10: } Chris@40: Chris@42: static std::string getErrorText() { Chris@38: DWORD err = GetLastError(); Chris@42: wchar_t *buffer = 0; Chris@38: FormatMessageW( Chris@10: FORMAT_MESSAGE_ALLOCATE_BUFFER | Chris@10: FORMAT_MESSAGE_FROM_SYSTEM | Chris@10: FORMAT_MESSAGE_IGNORE_INSERTS, Chris@10: NULL, Chris@10: err, Chris@42: // the correct way to specify the user's default language, Chris@42: // according to all resources I could find: Chris@42: MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), Chris@38: (LPWSTR) &buffer, Chris@10: 0, NULL ); Chris@42: if (!buffer) { Chris@42: return "Unable to format error string (internal error)"; Chris@42: } Chris@16: int wn = wcslen(buffer); Chris@16: int n = WideCharToMultiByte(CP_UTF8, 0, buffer, wn, 0, 0, 0, 0); Chris@17: if (n < 0) { Chris@46: LocalFree(buffer); Chris@17: return "Unable to convert error string (internal error)"; Chris@17: } Chris@16: char *text = new char[n+1]; Chris@17: (void)WideCharToMultiByte(CP_UTF8, 0, buffer, wn, text, n, 0, 0); Chris@16: text[n] = '\0'; Chris@10: std::string s(text); Chris@46: LocalFree(buffer); Chris@10: delete[] text; Chris@42: if (s == "") { Chris@42: return s; Chris@42: } Chris@10: for (int i = s.size(); i > 0; ) { Chris@10: --i; Chris@10: if (s[i] == '\n' || s[i] == '\r') { Chris@10: s.erase(i, 1); Chris@10: } Chris@10: } Chris@18: std::size_t pos = s.find("%1"); Chris@18: if (pos != std::string::npos && lastLibraryName != "") { Chris@18: s.replace(pos, 2, lastLibraryName); Chris@18: } Chris@10: return s; Chris@10: } Chris@40: Chris@42: #define DLOPEN(a,b) loadLibraryUTF8(a) Chris@10: #define DLSYM(a,b) (void *)GetProcAddress((HINSTANCE)(a),(b).c_str()) Chris@0: #define DLCLOSE(a) (!FreeLibrary((HINSTANCE)(a))) Chris@42: #define DLERROR() (getErrorText()) Chris@42: Chris@42: static bool libraryExists(std::string name) { Chris@42: if (name == "") return false; Chris@42: int n = name.size(); Chris@42: int wn = MultiByteToWideChar(CP_UTF8, 0, name.c_str(), n, 0, 0); Chris@42: wchar_t *wname = new wchar_t[wn+1]; Chris@42: wn = MultiByteToWideChar(CP_UTF8, 0, name.c_str(), n, wname, wn); Chris@42: wname[wn] = L'\0'; Chris@42: FILE *f = _wfopen(wname, L"rb"); Chris@42: delete[] wname; Chris@42: if (f) { Chris@42: fclose(f); Chris@42: return true; Chris@42: } else { Chris@42: return false; Chris@42: } Chris@42: } Chris@40: Chris@0: #else Chris@40: Chris@0: #include Chris@0: #define DLOPEN(a,b) dlopen((a).c_str(),(b)) Chris@0: #define DLSYM(a,b) dlsym((a),(b).c_str()) Chris@0: #define DLCLOSE(a) dlclose((a)) Chris@0: #define DLERROR() dlerror() Chris@40: Chris@42: static bool libraryExists(std::string name) { Chris@42: if (name == "") return false; Chris@42: FILE *f = fopen(name.c_str(), "r"); Chris@42: if (f) { Chris@42: fclose(f); Chris@42: return true; Chris@42: } else { Chris@42: return false; Chris@42: } Chris@42: } Chris@42: Chris@0: #endif Chris@0: Chris@12: //#include Chris@6: Chris@0: using namespace std; Chris@0: Chris@0: string error() Chris@0: { Chris@40: return DLERROR(); Chris@0: } Chris@0: Chris@40: struct Result { Chris@40: PluginCheckCode code; Chris@40: string message; Chris@40: }; Chris@40: Chris@40: Result checkLADSPAStyleDescriptorFn(void *f) Chris@23: { Chris@23: typedef const void *(*DFn)(unsigned long); Chris@23: DFn fn = DFn(f); Chris@23: unsigned long index = 0; Chris@23: while (fn(index)) ++index; Chris@40: if (index == 0) return { PluginCheckCode::FAIL_NO_PLUGINS, "" }; Chris@40: return { PluginCheckCode::SUCCESS, "" }; Chris@23: } Chris@23: Chris@40: Result checkVampDescriptorFn(void *f) Chris@23: { Chris@23: typedef const void *(*DFn)(unsigned int, unsigned int); Chris@23: DFn fn = DFn(f); Chris@23: unsigned int index = 0; Chris@25: while (fn(2, index)) ++index; Chris@40: if (index == 0) return { PluginCheckCode::FAIL_NO_PLUGINS, "" }; Chris@40: return { PluginCheckCode::SUCCESS, "" }; Chris@23: } Chris@23: Chris@40: Result check(string soname, string descriptor) Chris@0: { Chris@0: void *handle = DLOPEN(soname, RTLD_NOW | RTLD_LOCAL); Chris@0: if (!handle) { Chris@40: PluginCheckCode code = PluginCheckCode::FAIL_NOT_LOADABLE; Chris@40: string message = error(); Chris@40: #ifdef _WIN32 Chris@40: DWORD err = GetLastError(); Chris@40: if (err == ERROR_BAD_EXE_FORMAT) { Chris@40: code = PluginCheckCode::FAIL_WRONG_ARCHITECTURE; Chris@40: } else if (err == ERROR_MOD_NOT_FOUND) { Chris@42: if (libraryExists(soname)) { Chris@42: code = PluginCheckCode::FAIL_DEPENDENCY_MISSING; Chris@42: } else { Chris@42: code = PluginCheckCode::FAIL_LIBRARY_NOT_FOUND; Chris@42: } Chris@42: } Chris@52: #else // !_WIN32 Chris@52: #ifdef __APPLE__ Chris@52: if (errno == EPERM) { Chris@51: // This may be unreliable, but it seems to be set by Chris@51: // something dlopen() calls in the case where a library Chris@51: // can't be loaded for code-signing-related reasons on Chris@51: // macOS Chris@51: code = PluginCheckCode::FAIL_FORBIDDEN; Chris@52: } else if (!libraryExists(soname)) { Chris@52: code = PluginCheckCode::FAIL_LIBRARY_NOT_FOUND; Chris@40: } Chris@52: #else // !__APPLE__ Chris@52: if (!libraryExists(soname)) { Chris@52: code = PluginCheckCode::FAIL_LIBRARY_NOT_FOUND; Chris@52: } Chris@52: #endif // !__APPLE__ Chris@52: #endif // !_WIN32 Chris@52: Chris@40: return { code, message }; Chris@0: } Chris@0: Chris@40: Result result { PluginCheckCode::SUCCESS, "" }; Chris@38: Chris@0: void *fn = DLSYM(handle, descriptor); Chris@0: if (!fn) { Chris@40: result = { PluginCheckCode::FAIL_DESCRIPTOR_MISSING, error() }; Chris@38: } else if (descriptor == "ladspa_descriptor") { Chris@40: result = checkLADSPAStyleDescriptorFn(fn); Chris@23: } else if (descriptor == "dssi_descriptor") { Chris@40: result = checkLADSPAStyleDescriptorFn(fn); Chris@23: } else if (descriptor == "vampGetPluginDescriptor") { Chris@40: result = checkVampDescriptorFn(fn); Chris@23: } else { Chris@23: cerr << "Note: no descriptor logic known for descriptor function \"" Chris@23: << descriptor << "\"; not actually calling it" << endl; Chris@23: } Chris@38: Chris@38: DLCLOSE(handle); Chris@23: Chris@40: return result; Chris@0: } Chris@0: Chris@49: // We write our output to stdout, but want to ensure that the plugin Chris@49: // doesn't write anything itself. To do this we open a null file Chris@49: // descriptor and dup2() it into place of stdout in the gaps between Chris@49: // our own output activity. Chris@49: Chris@49: static int normalFd = -1; Chris@49: static int suspendedFd = -1; Chris@49: Chris@49: static void initFds() Chris@49: { Chris@49: #ifdef _WIN32 Chris@49: normalFd = _dup(1); Chris@49: suspendedFd = _open("NUL", _O_WRONLY); Chris@49: #else Chris@49: normalFd = dup(1); Chris@49: suspendedFd = open("/dev/null", O_WRONLY); Chris@49: #endif Chris@49: Chris@49: if (normalFd < 0 || suspendedFd < 0) { Chris@50: throw std::runtime_error Chris@50: ("Failed to initialise fds for stdio suspend/resume"); Chris@49: } Chris@49: } Chris@49: Chris@49: static void suspendOutput() Chris@49: { Chris@49: #ifdef _WIN32 Chris@49: _dup2(suspendedFd, 1); Chris@49: #else Chris@49: dup2(suspendedFd, 1); Chris@49: #endif Chris@49: } Chris@49: Chris@49: static void resumeOutput() Chris@49: { Chris@49: fflush(stdout); Chris@49: #ifdef _WIN32 Chris@49: _dup2(normalFd, 1); Chris@49: #else Chris@49: dup2(normalFd, 1); Chris@49: #endif Chris@49: } Chris@49: Chris@0: int main(int argc, char **argv) Chris@0: { Chris@0: bool allGood = true; Chris@0: string soname; Chris@0: Chris@27: bool showUsage = false; Chris@27: Chris@27: if (argc > 1) { Chris@27: string opt = argv[1]; Chris@27: if (opt == "-?" || opt == "-h" || opt == "--help") { Chris@27: showUsage = true; Chris@27: } else if (opt == "-v" || opt == "--version") { Chris@40: cout << CHECKER_COMPATIBILITY_VERSION << endl; Chris@27: return 0; Chris@27: } Chris@27: } Chris@27: Chris@27: if (argc != 2 || showUsage) { Chris@27: cerr << endl; Chris@28: cerr << programName << ": Test shared library objects for plugins to be" << endl; Chris@27: cerr << "loaded via descriptor functions." << endl; Chris@28: cerr << "\n Usage: " << programName << " \n" Chris@11: "\nwhere descriptorname is the name of a plugin descriptor symbol to be sought\n" Chris@11: "in each library (e.g. vampGetPluginDescriptor for Vamp plugins). The list of\n" Chris@11: "candidate plugin library filenames is read from stdin.\n" << endl; Chris@11: return 2; Chris@1: } Chris@1: Chris@1: string descriptor = argv[1]; Chris@1: Chris@32: #ifdef _WIN32 Chris@32: // Avoid showing the error-handler dialog for missing DLLs, Chris@32: // failing quietly instead. It's permissible for this program Chris@32: // to simply fail when a DLL can't be loaded -- showing the Chris@32: // error dialog wouldn't change this anyway, it would just Chris@32: // block the program until the user clicked it away and then Chris@32: // fail anyway. Chris@32: SetErrorMode(SEM_FAILCRITICALERRORS); Chris@32: #endif Chris@32: Chris@49: initFds(); Chris@49: suspendOutput(); Chris@49: Chris@0: while (getline(cin, soname)) { Chris@40: Result result = check(soname, descriptor); Chris@49: resumeOutput(); Chris@40: if (result.code == PluginCheckCode::SUCCESS) { Chris@40: cout << "SUCCESS|" << soname << "|" << endl; Chris@40: } else { Chris@40: if (result.message == "") { Chris@40: cout << "FAILURE|" << soname Chris@40: << "|[" << int(result.code) << "]" << endl; Chris@40: } else { Chris@51: for (size_t i = 0; i < result.message.size(); ++i) { Chris@51: if (result.message[i] == '\n' || Chris@51: result.message[i] == '\r') { Chris@51: result.message[i] = ' '; Chris@51: } Chris@51: } Chris@40: cout << "FAILURE|" << soname Chris@40: << "|" << result.message << " [" Chris@40: << int(result.code) << "]" << endl; Chris@40: } Chris@11: allGood = false; Chris@11: } Chris@49: suspendOutput(); Chris@0: } Chris@11: Chris@0: return allGood ? 0 : 1; Chris@0: }