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