annotate src/plugincandidates.cpp @ 64:e839338d3869 tip

Further Windows fix
author Chris Cannam
date Wed, 15 Apr 2020 16:30:40 +0100
parents ef64b3f171d9
children
rev   line source
Chris@2 1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
Chris@5 2 /*
Chris@40 3 Copyright (c) 2016-2018 Queen Mary, University of London
Chris@5 4
Chris@19 5 Permission is hereby granted, free of charge, to any person
Chris@19 6 obtaining a copy of this software and associated documentation
Chris@19 7 files (the "Software"), to deal in the Software without
Chris@19 8 restriction, including without limitation the rights to use, copy,
Chris@19 9 modify, merge, publish, distribute, sublicense, and/or sell copies
Chris@19 10 of the Software, and to permit persons to whom the Software is
Chris@19 11 furnished to do so, subject to the following conditions:
Chris@5 12
Chris@19 13 The above copyright notice and this permission notice shall be
Chris@19 14 included in all copies or substantial portions of the Software.
Chris@5 15
Chris@19 16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
Chris@19 17 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
Chris@19 18 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
Chris@19 19 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
Chris@19 20 CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
Chris@19 21 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
Chris@19 22 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Chris@5 23
Chris@19 24 Except as contained in this notice, the names of the Centre for
Chris@19 25 Digital Music and Queen Mary, University of London shall not be
Chris@19 26 used in advertising or otherwise to promote the sale, use or other
Chris@19 27 dealings in this Software without prior written authorization.
Chris@5 28 */
Chris@2 29
Chris@2 30 #include "plugincandidates.h"
Chris@2 31
Chris@40 32 #include "../version.h"
Chris@40 33
Chris@2 34 #include <set>
Chris@2 35 #include <stdexcept>
Chris@2 36 #include <iostream>
Chris@2 37
Chris@2 38 #include <QProcess>
Chris@2 39 #include <QDir>
Chris@61 40 #include <QElapsedTimer>
Chris@2 41
Chris@4 42 #if defined(_WIN32)
Chris@2 43 #define PLUGIN_GLOB "*.dll"
Chris@4 44 #elif defined(__APPLE__)
Chris@2 45 #define PLUGIN_GLOB "*.dylib *.so"
Chris@2 46 #else
Chris@2 47 #define PLUGIN_GLOB "*.so"
Chris@2 48 #endif
Chris@2 49
Chris@2 50 using namespace std;
Chris@2 51
Chris@2 52 PluginCandidates::PluginCandidates(string helperExecutableName) :
Chris@6 53 m_helper(helperExecutableName),
Chris@53 54 m_logCallback(nullptr)
Chris@2 55 {
Chris@2 56 }
Chris@2 57
Chris@6 58 void
Chris@6 59 PluginCandidates::setLogCallback(LogCallback *cb)
Chris@6 60 {
Chris@6 61 m_logCallback = cb;
Chris@6 62 }
Chris@6 63
Chris@2 64 vector<string>
Chris@4 65 PluginCandidates::getCandidateLibrariesFor(string tag) const
Chris@2 66 {
Chris@4 67 if (m_candidates.find(tag) == m_candidates.end()) return {};
Chris@4 68 else return m_candidates.at(tag);
Chris@2 69 }
Chris@2 70
Chris@2 71 vector<PluginCandidates::FailureRec>
Chris@4 72 PluginCandidates::getFailedLibrariesFor(string tag) const
Chris@2 73 {
Chris@4 74 if (m_failures.find(tag) == m_failures.end()) return {};
Chris@4 75 else return m_failures.at(tag);
Chris@2 76 }
Chris@2 77
Chris@6 78 void
Chris@6 79 PluginCandidates::log(string message)
Chris@6 80 {
Chris@24 81 if (m_logCallback) {
Chris@24 82 m_logCallback->log("PluginCandidates: " + message);
Chris@24 83 } else {
Chris@24 84 cerr << "PluginCandidates: " << message << endl;
Chris@24 85 }
Chris@6 86 }
Chris@6 87
Chris@2 88 vector<string>
Chris@2 89 PluginCandidates::getLibrariesInPath(vector<string> path)
Chris@2 90 {
Chris@2 91 vector<string> candidates;
Chris@2 92
Chris@2 93 for (string dirname: path) {
Chris@2 94
Chris@55 95 log("Scanning directory " + dirname);
Chris@2 96
Chris@2 97 QDir dir(dirname.c_str(), PLUGIN_GLOB,
Chris@19 98 QDir::Name | QDir::IgnoreCase,
Chris@19 99 QDir::Files | QDir::Readable);
Chris@2 100
Chris@19 101 for (unsigned int i = 0; i < dir.count(); ++i) {
Chris@2 102 QString soname = dir.filePath(dir[i]);
Chris@11 103 // NB this means the library names passed to the helper
Chris@11 104 // are UTF-8 encoded
Chris@2 105 candidates.push_back(soname.toStdString());
Chris@2 106 }
Chris@2 107 }
Chris@2 108
Chris@2 109 return candidates;
Chris@2 110 }
Chris@2 111
Chris@2 112 void
Chris@2 113 PluginCandidates::scan(string tag,
Chris@19 114 vector<string> pluginPath,
Chris@19 115 string descriptorSymbolName)
Chris@2 116 {
Chris@40 117 string helperVersion = getHelperCompatibilityVersion();
Chris@40 118 if (helperVersion != CHECKER_COMPATIBILITY_VERSION) {
Chris@55 119 log("Wrong plugin checker helper version found: expected v" +
Chris@40 120 string(CHECKER_COMPATIBILITY_VERSION) + ", found v" +
Chris@40 121 helperVersion);
Chris@43 122 throw runtime_error("wrong version of plugin load helper found");
Chris@40 123 }
Chris@40 124
Chris@2 125 vector<string> libraries = getLibrariesInPath(pluginPath);
Chris@2 126 vector<string> remaining = libraries;
Chris@2 127
Chris@2 128 int runlimit = 20;
Chris@2 129 int runcount = 0;
Chris@2 130
Chris@2 131 vector<string> result;
Chris@2 132
Chris@2 133 while (result.size() < libraries.size() && runcount < runlimit) {
Chris@19 134 vector<string> output = runHelper(remaining, descriptorSymbolName);
Chris@19 135 result.insert(result.end(), output.begin(), output.end());
Chris@19 136 int shortfall = int(remaining.size()) - int(output.size());
Chris@19 137 if (shortfall > 0) {
Chris@19 138 // Helper bailed out for some reason presumably associated
Chris@19 139 // with the plugin following the last one it reported
Chris@19 140 // on. Add a failure entry for that one and continue with
Chris@19 141 // the following ones.
Chris@6 142 string failed = *(remaining.rbegin() + shortfall - 1);
Chris@55 143 log("Helper output ended before result for plugin " + failed);
Chris@19 144 result.push_back("FAILURE|" + failed + "|Plugin load check failed or timed out");
Chris@4 145 remaining = vector<string>
Chris@4 146 (remaining.rbegin(), remaining.rbegin() + shortfall - 1);
Chris@19 147 }
Chris@19 148 ++runcount;
Chris@2 149 }
Chris@2 150
Chris@2 151 recordResult(tag, result);
Chris@2 152 }
Chris@2 153
Chris@40 154 string
Chris@40 155 PluginCandidates::getHelperCompatibilityVersion()
Chris@2 156 {
Chris@2 157 QProcess process;
Chris@2 158 process.setReadChannel(QProcess::StandardOutput);
Chris@4 159 process.setProcessChannelMode(QProcess::ForwardedErrorChannel);
Chris@40 160 process.start(m_helper.c_str(), { "--version" });
Chris@40 161
Chris@2 162 if (!process.waitForStarted()) {
cannam@13 163 QProcess::ProcessError err = process.error();
cannam@13 164 if (err == QProcess::FailedToStart) {
cannam@13 165 std::cerr << "Unable to start helper process " << m_helper
cannam@13 166 << std::endl;
cannam@13 167 } else if (err == QProcess::Crashed) {
cannam@13 168 std::cerr << "Helper process " << m_helper
cannam@13 169 << " crashed on startup" << std::endl;
cannam@13 170 } else {
cannam@13 171 std::cerr << "Helper process " << m_helper
cannam@13 172 << " failed on startup with error code "
cannam@13 173 << err << std::endl;
cannam@13 174 }
Chris@19 175 throw runtime_error("plugin load helper failed to start");
Chris@2 176 }
Chris@40 177 process.waitForFinished();
Chris@40 178
Chris@40 179 QByteArray output = process.readAllStandardOutput();
Chris@40 180 while (output.endsWith('\n') || output.endsWith('\r')) {
Chris@40 181 output.chop(1);
Chris@40 182 }
Chris@40 183
Chris@44 184 string versionString = QString(output).toStdString();
Chris@55 185 log("Read version string from helper: " + versionString);
Chris@40 186 return versionString;
Chris@40 187 }
Chris@40 188
Chris@40 189 vector<string>
Chris@40 190 PluginCandidates::runHelper(vector<string> libraries, string descriptor)
Chris@40 191 {
Chris@40 192 vector<string> output;
Chris@40 193
Chris@55 194 log("Running helper " + m_helper + " with following library list:");
Chris@40 195 for (auto &lib: libraries) log(lib);
Chris@40 196
Chris@40 197 QProcess process;
Chris@40 198 process.setReadChannel(QProcess::StandardOutput);
Chris@55 199
Chris@55 200 if (m_logCallback) {
Chris@55 201 log("Log callback is set: using separate-channels mode to gather stderr");
Chris@55 202 process.setProcessChannelMode(QProcess::SeparateChannels);
Chris@55 203 } else {
Chris@55 204 process.setProcessChannelMode(QProcess::ForwardedErrorChannel);
Chris@55 205 }
Chris@55 206
Chris@40 207 process.start(m_helper.c_str(), { descriptor.c_str() });
Chris@40 208
Chris@40 209 if (!process.waitForStarted()) {
Chris@40 210 QProcess::ProcessError err = process.error();
Chris@40 211 if (err == QProcess::FailedToStart) {
Chris@40 212 std::cerr << "Unable to start helper process " << m_helper
Chris@40 213 << std::endl;
Chris@40 214 } else if (err == QProcess::Crashed) {
Chris@40 215 std::cerr << "Helper process " << m_helper
Chris@40 216 << " crashed on startup" << std::endl;
Chris@40 217 } else {
Chris@40 218 std::cerr << "Helper process " << m_helper
Chris@40 219 << " failed on startup with error code "
Chris@40 220 << err << std::endl;
Chris@40 221 }
Chris@55 222 logErrors(&process);
Chris@40 223 throw runtime_error("plugin load helper failed to start");
Chris@40 224 }
Chris@55 225
Chris@55 226 log("Helper " + m_helper + " started OK");
Chris@55 227 logErrors(&process);
Chris@40 228
Chris@2 229 for (auto &lib: libraries) {
Chris@19 230 process.write(lib.c_str(), lib.size());
Chris@19 231 process.write("\n", 1);
Chris@2 232 }
Chris@2 233
Chris@61 234 QElapsedTimer t;
Chris@6 235 t.start();
Chris@33 236 int timeout = 15000; // ms
Chris@6 237
Chris@12 238 const int buflen = 4096;
Chris@4 239 bool done = false;
Chris@4 240
Chris@4 241 while (!done) {
Chris@19 242 char buf[buflen];
Chris@19 243 qint64 linelen = process.readLine(buf, buflen);
Chris@4 244 if (linelen > 0) {
Chris@4 245 output.push_back(buf);
Chris@4 246 done = (output.size() == libraries.size());
Chris@4 247 } else if (linelen < 0) {
Chris@4 248 // error case
Chris@55 249 log("Received error code while reading from helper");
Chris@4 250 done = true;
Chris@19 251 } else {
Chris@4 252 // no error, but no line read (could just be between
Chris@4 253 // lines, or could be eof)
Chris@4 254 done = (process.state() == QProcess::NotRunning);
Chris@6 255 if (!done) {
Chris@6 256 if (t.elapsed() > timeout) {
Chris@6 257 // this is purely an emergency measure
Chris@55 258 log("Timeout: helper took too long, killing it");
Chris@6 259 process.kill();
Chris@6 260 done = true;
Chris@6 261 } else {
Chris@6 262 process.waitForReadyRead(200);
Chris@6 263 }
Chris@6 264 }
Chris@2 265 }
Chris@55 266 logErrors(&process);
Chris@4 267 }
Chris@4 268
Chris@4 269 if (process.state() != QProcess::NotRunning) {
Chris@4 270 process.close();
Chris@4 271 process.waitForFinished();
Chris@55 272 logErrors(&process);
Chris@4 273 }
cannam@13 274
Chris@55 275 log("Helper completed");
cannam@13 276
Chris@2 277 return output;
Chris@2 278 }
Chris@2 279
Chris@2 280 void
Chris@55 281 PluginCandidates::logErrors(QProcess *p)
Chris@55 282 {
Chris@55 283 p->setReadChannel(QProcess::StandardError);
Chris@55 284
Chris@55 285 qint64 byteCount = p->bytesAvailable();
Chris@55 286 if (byteCount == 0) {
Chris@55 287 p->setReadChannel(QProcess::StandardOutput);
Chris@55 288 return;
Chris@55 289 }
Chris@55 290
Chris@55 291 QByteArray buffer = p->read(byteCount);
Chris@55 292 while (buffer.endsWith('\n') || buffer.endsWith('\r')) {
Chris@55 293 buffer.chop(1);
Chris@55 294 }
Chris@55 295 std::string str(buffer.constData(), buffer.size());
Chris@55 296 log("Helper stderr output follows:\n" + str);
Chris@55 297 log("Helper stderr output ends");
Chris@55 298
Chris@55 299 p->setReadChannel(QProcess::StandardOutput);
Chris@55 300 }
Chris@55 301
Chris@55 302 void
Chris@2 303 PluginCandidates::recordResult(string tag, vector<string> result)
Chris@2 304 {
Chris@4 305 for (auto &r: result) {
Chris@4 306
Chris@4 307 QString s(r.c_str());
Chris@4 308 QStringList bits = s.split("|");
Chris@6 309
Chris@55 310 log(("Read output line from helper: " + s.trimmed()).toStdString());
Chris@6 311
Chris@4 312 if (bits.size() < 2 || bits.size() > 3) {
Chris@55 313 log("Invalid output line (wrong number of |-separated fields)");
Chris@4 314 continue;
Chris@4 315 }
Chris@4 316
Chris@4 317 string status = bits[0].toStdString();
Chris@4 318
Chris@4 319 string library = bits[1].toStdString();
Chris@40 320 if (bits.size() == 2) {
Chris@40 321 library = bits[1].trimmed().toStdString();
Chris@40 322 }
Chris@4 323
Chris@4 324 if (status == "SUCCESS") {
Chris@4 325 m_candidates[tag].push_back(library);
Chris@4 326
Chris@4 327 } else if (status == "FAILURE") {
Chris@40 328
Chris@40 329 QString messageAndCode = "";
Chris@40 330 if (bits.size() > 2) {
Chris@40 331 messageAndCode = bits[2].trimmed();
Chris@40 332 }
Chris@40 333
Chris@40 334 PluginCheckCode code = PluginCheckCode::FAIL_OTHER;
Chris@40 335 string message = "";
Chris@40 336
Chris@40 337 QRegExp codeRE("^(.*) *\\[([0-9]+)\\]$");
Chris@40 338 if (codeRE.exactMatch(messageAndCode)) {
Chris@40 339 QStringList caps(codeRE.capturedTexts());
Chris@40 340 if (caps.length() == 3) {
Chris@40 341 message = caps[1].toStdString();
Chris@40 342 code = PluginCheckCode(caps[2].toInt());
Chris@55 343 log("Split failure report into message and failure code "
Chris@40 344 + caps[2].toStdString());
Chris@40 345 } else {
Chris@55 346 log("Unable to split out failure code from report");
Chris@40 347 }
Chris@40 348 } else {
Chris@55 349 log("Failure message does not give a failure code");
Chris@40 350 }
Chris@40 351
Chris@40 352 if (message == "") {
Chris@40 353 message = messageAndCode.toStdString();
Chris@40 354 }
Chris@40 355
Chris@40 356 m_failures[tag].push_back({ library, code, message });
Chris@4 357
Chris@4 358 } else {
Chris@55 359 log("Unexpected status \"" + status + "\" in output line");
Chris@4 360 }
Chris@4 361 }
Chris@2 362 }
Chris@2 363