Chris@2
|
1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
|
Chris@5
|
2 /*
|
Chris@19
|
3 Copyright (c) 2016 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@2
|
32 #include <set>
|
Chris@2
|
33 #include <stdexcept>
|
Chris@2
|
34 #include <iostream>
|
Chris@2
|
35
|
Chris@2
|
36 #include <QProcess>
|
Chris@2
|
37 #include <QDir>
|
Chris@6
|
38 #include <QTime>
|
Chris@2
|
39
|
Chris@4
|
40 #if defined(_WIN32)
|
Chris@2
|
41 #define PLUGIN_GLOB "*.dll"
|
Chris@4
|
42 #elif defined(__APPLE__)
|
Chris@2
|
43 #define PLUGIN_GLOB "*.dylib *.so"
|
Chris@2
|
44 #else
|
Chris@2
|
45 #define PLUGIN_GLOB "*.so"
|
Chris@2
|
46 #endif
|
Chris@2
|
47
|
Chris@2
|
48 using namespace std;
|
Chris@2
|
49
|
Chris@2
|
50 PluginCandidates::PluginCandidates(string helperExecutableName) :
|
Chris@6
|
51 m_helper(helperExecutableName),
|
Chris@6
|
52 m_logCallback(0)
|
Chris@2
|
53 {
|
Chris@2
|
54 }
|
Chris@2
|
55
|
Chris@6
|
56 void
|
Chris@6
|
57 PluginCandidates::setLogCallback(LogCallback *cb)
|
Chris@6
|
58 {
|
Chris@6
|
59 m_logCallback = cb;
|
Chris@6
|
60 }
|
Chris@6
|
61
|
Chris@2
|
62 vector<string>
|
Chris@4
|
63 PluginCandidates::getCandidateLibrariesFor(string tag) const
|
Chris@2
|
64 {
|
Chris@4
|
65 if (m_candidates.find(tag) == m_candidates.end()) return {};
|
Chris@4
|
66 else return m_candidates.at(tag);
|
Chris@2
|
67 }
|
Chris@2
|
68
|
Chris@2
|
69 vector<PluginCandidates::FailureRec>
|
Chris@4
|
70 PluginCandidates::getFailedLibrariesFor(string tag) const
|
Chris@2
|
71 {
|
Chris@4
|
72 if (m_failures.find(tag) == m_failures.end()) return {};
|
Chris@4
|
73 else return m_failures.at(tag);
|
Chris@2
|
74 }
|
Chris@2
|
75
|
Chris@6
|
76 void
|
Chris@6
|
77 PluginCandidates::log(string message)
|
Chris@6
|
78 {
|
Chris@24
|
79 if (m_logCallback) {
|
Chris@24
|
80 m_logCallback->log("PluginCandidates: " + message);
|
Chris@24
|
81 } else {
|
Chris@24
|
82 cerr << "PluginCandidates: " << message << endl;
|
Chris@24
|
83 }
|
Chris@6
|
84 }
|
Chris@6
|
85
|
Chris@2
|
86 vector<string>
|
Chris@2
|
87 PluginCandidates::getLibrariesInPath(vector<string> path)
|
Chris@2
|
88 {
|
Chris@2
|
89 vector<string> candidates;
|
Chris@2
|
90
|
Chris@2
|
91 for (string dirname: path) {
|
Chris@2
|
92
|
Chris@24
|
93 log("scanning directory " + dirname);
|
Chris@2
|
94
|
Chris@2
|
95 QDir dir(dirname.c_str(), PLUGIN_GLOB,
|
Chris@19
|
96 QDir::Name | QDir::IgnoreCase,
|
Chris@19
|
97 QDir::Files | QDir::Readable);
|
Chris@2
|
98
|
Chris@19
|
99 for (unsigned int i = 0; i < dir.count(); ++i) {
|
Chris@2
|
100 QString soname = dir.filePath(dir[i]);
|
Chris@11
|
101 // NB this means the library names passed to the helper
|
Chris@11
|
102 // are UTF-8 encoded
|
Chris@2
|
103 candidates.push_back(soname.toStdString());
|
Chris@2
|
104 }
|
Chris@2
|
105 }
|
Chris@2
|
106
|
Chris@2
|
107 return candidates;
|
Chris@2
|
108 }
|
Chris@2
|
109
|
Chris@2
|
110 void
|
Chris@2
|
111 PluginCandidates::scan(string tag,
|
Chris@19
|
112 vector<string> pluginPath,
|
Chris@19
|
113 string descriptorSymbolName)
|
Chris@2
|
114 {
|
Chris@2
|
115 vector<string> libraries = getLibrariesInPath(pluginPath);
|
Chris@2
|
116 vector<string> remaining = libraries;
|
Chris@2
|
117
|
Chris@2
|
118 int runlimit = 20;
|
Chris@2
|
119 int runcount = 0;
|
Chris@2
|
120
|
Chris@2
|
121 vector<string> result;
|
Chris@2
|
122
|
Chris@2
|
123 while (result.size() < libraries.size() && runcount < runlimit) {
|
Chris@19
|
124 vector<string> output = runHelper(remaining, descriptorSymbolName);
|
Chris@19
|
125 result.insert(result.end(), output.begin(), output.end());
|
Chris@19
|
126 int shortfall = int(remaining.size()) - int(output.size());
|
Chris@19
|
127 if (shortfall > 0) {
|
Chris@19
|
128 // Helper bailed out for some reason presumably associated
|
Chris@19
|
129 // with the plugin following the last one it reported
|
Chris@19
|
130 // on. Add a failure entry for that one and continue with
|
Chris@19
|
131 // the following ones.
|
Chris@6
|
132 string failed = *(remaining.rbegin() + shortfall - 1);
|
Chris@24
|
133 log("helper output ended before result for plugin " + failed);
|
Chris@19
|
134 result.push_back("FAILURE|" + failed + "|Plugin load check failed or timed out");
|
Chris@4
|
135 remaining = vector<string>
|
Chris@4
|
136 (remaining.rbegin(), remaining.rbegin() + shortfall - 1);
|
Chris@19
|
137 }
|
Chris@19
|
138 ++runcount;
|
Chris@2
|
139 }
|
Chris@2
|
140
|
Chris@2
|
141 recordResult(tag, result);
|
Chris@2
|
142 }
|
Chris@2
|
143
|
Chris@2
|
144 vector<string>
|
Chris@2
|
145 PluginCandidates::runHelper(vector<string> libraries, string descriptor)
|
Chris@2
|
146 {
|
Chris@2
|
147 vector<string> output;
|
Chris@6
|
148
|
Chris@24
|
149 log("running helper " + m_helper + " with following library list:");
|
Chris@24
|
150 for (auto &lib: libraries) log(lib);
|
Chris@2
|
151
|
Chris@2
|
152 QProcess process;
|
Chris@2
|
153 process.setReadChannel(QProcess::StandardOutput);
|
Chris@4
|
154 process.setProcessChannelMode(QProcess::ForwardedErrorChannel);
|
Chris@2
|
155 process.start(m_helper.c_str(), { descriptor.c_str() });
|
Chris@2
|
156 if (!process.waitForStarted()) {
|
cannam@13
|
157 QProcess::ProcessError err = process.error();
|
cannam@13
|
158 if (err == QProcess::FailedToStart) {
|
cannam@13
|
159 std::cerr << "Unable to start helper process " << m_helper
|
cannam@13
|
160 << std::endl;
|
cannam@13
|
161 } else if (err == QProcess::Crashed) {
|
cannam@13
|
162 std::cerr << "Helper process " << m_helper
|
cannam@13
|
163 << " crashed on startup" << std::endl;
|
cannam@13
|
164 } else {
|
cannam@13
|
165 std::cerr << "Helper process " << m_helper
|
cannam@13
|
166 << " failed on startup with error code "
|
cannam@13
|
167 << err << std::endl;
|
cannam@13
|
168 }
|
Chris@19
|
169 throw runtime_error("plugin load helper failed to start");
|
Chris@2
|
170 }
|
Chris@2
|
171 for (auto &lib: libraries) {
|
Chris@19
|
172 process.write(lib.c_str(), lib.size());
|
Chris@19
|
173 process.write("\n", 1);
|
Chris@2
|
174 }
|
Chris@2
|
175
|
Chris@6
|
176 QTime t;
|
Chris@6
|
177 t.start();
|
Chris@6
|
178 int timeout = 3000; // ms
|
Chris@6
|
179
|
Chris@12
|
180 const int buflen = 4096;
|
Chris@4
|
181 bool done = false;
|
Chris@4
|
182
|
Chris@4
|
183 while (!done) {
|
Chris@19
|
184 char buf[buflen];
|
Chris@19
|
185 qint64 linelen = process.readLine(buf, buflen);
|
Chris@4
|
186 if (linelen > 0) {
|
Chris@4
|
187 output.push_back(buf);
|
Chris@4
|
188 done = (output.size() == libraries.size());
|
Chris@4
|
189 } else if (linelen < 0) {
|
Chris@4
|
190 // error case
|
Chris@24
|
191 log("received error code while reading from helper");
|
Chris@4
|
192 done = true;
|
Chris@19
|
193 } else {
|
Chris@4
|
194 // no error, but no line read (could just be between
|
Chris@4
|
195 // lines, or could be eof)
|
Chris@4
|
196 done = (process.state() == QProcess::NotRunning);
|
Chris@6
|
197 if (!done) {
|
Chris@6
|
198 if (t.elapsed() > timeout) {
|
Chris@6
|
199 // this is purely an emergency measure
|
Chris@24
|
200 log("timeout: helper took too long, killing it");
|
Chris@6
|
201 process.kill();
|
Chris@6
|
202 done = true;
|
Chris@6
|
203 } else {
|
Chris@6
|
204 process.waitForReadyRead(200);
|
Chris@6
|
205 }
|
Chris@6
|
206 }
|
Chris@2
|
207 }
|
Chris@4
|
208 }
|
Chris@4
|
209
|
Chris@4
|
210 if (process.state() != QProcess::NotRunning) {
|
Chris@4
|
211 process.close();
|
Chris@4
|
212 process.waitForFinished();
|
Chris@4
|
213 }
|
cannam@13
|
214
|
Chris@24
|
215 log("helper completed");
|
cannam@13
|
216
|
Chris@2
|
217 return output;
|
Chris@2
|
218 }
|
Chris@2
|
219
|
Chris@2
|
220 void
|
Chris@2
|
221 PluginCandidates::recordResult(string tag, vector<string> result)
|
Chris@2
|
222 {
|
Chris@4
|
223 for (auto &r: result) {
|
Chris@4
|
224
|
Chris@4
|
225 QString s(r.c_str());
|
Chris@4
|
226 QStringList bits = s.split("|");
|
Chris@6
|
227
|
Chris@24
|
228 log(("read output line from helper: " + s.trimmed()).toStdString());
|
Chris@6
|
229
|
Chris@4
|
230 if (bits.size() < 2 || bits.size() > 3) {
|
Chris@24
|
231 log("invalid output line (wrong number of |-separated fields)");
|
Chris@4
|
232 continue;
|
Chris@4
|
233 }
|
Chris@4
|
234
|
Chris@4
|
235 string status = bits[0].toStdString();
|
Chris@4
|
236
|
Chris@4
|
237 string library = bits[1].toStdString();
|
Chris@4
|
238 if (bits.size() == 2) library = bits[1].trimmed().toStdString();
|
Chris@4
|
239
|
Chris@4
|
240 string message = "";
|
Chris@4
|
241 if (bits.size() > 2) message = bits[2].trimmed().toStdString();
|
Chris@4
|
242
|
Chris@4
|
243 if (status == "SUCCESS") {
|
Chris@4
|
244 m_candidates[tag].push_back(library);
|
Chris@4
|
245
|
Chris@4
|
246 } else if (status == "FAILURE") {
|
Chris@4
|
247 m_failures[tag].push_back({ library, message });
|
Chris@4
|
248
|
Chris@4
|
249 } else {
|
Chris@24
|
250 log("unexpected status \"" + status + "\" in output line");
|
Chris@4
|
251 }
|
Chris@4
|
252 }
|
Chris@2
|
253 }
|
Chris@2
|
254
|