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
|