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