1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
2 
3 /*
4     Sonic Visualiser
5     An audio file viewer and annotation editor.
6     Centre for Digital Music, Queen Mary, University of London.
7     This file copyright 2006-2016 Chris Cannam and QMUL.
8 
9     This program is free software; you can redistribute it and/or
10     modify it under the terms of the GNU General Public License as
11     published by the Free Software Foundation; either version 2 of the
12     License, or (at your option) any later version.  See the file
13     COPYING included with this distribution for more information.
14 */
15 
16 #ifdef HAVE_PIPER
17 
18 #include "PiperVampPluginFactory.h"
19 #include "PluginIdentifier.h"
20 
21 #include "system/System.h"
22 
23 #include "PluginScan.h"
24 
25 #ifdef _WIN32
26 #undef VOID
27 #undef ERROR
28 #define CAPNP_LITE 1
29 #endif
30 
31 #include "vamp-client/qt/PiperAutoPlugin.h"
32 #include "vamp-client/qt/ProcessQtTransport.h"
33 #include "vamp-client/CapnpRRClient.h"
34 
35 #include <QDir>
36 #include <QFile>
37 #include <QFileInfo>
38 #include <QTextStream>
39 #include <QCoreApplication>
40 
41 #include <iostream>
42 
43 #include "base/Profiler.h"
44 #include "base/HelperExecPath.h"
45 
46 using namespace std;
47 
48 //#define DEBUG_PLUGIN_SCAN_AND_INSTANTIATE 1
49 
50 class PiperVampPluginFactory::Logger : public piper_vamp::client::LogCallback {
51 protected:
log(std::string message) const52     void log(std::string message) const override {
53         SVDEBUG << "PiperVampPluginFactory: " << message << endl;
54     }
55 };
56 
PiperVampPluginFactory()57 PiperVampPluginFactory::PiperVampPluginFactory() :
58     m_logger(new Logger)
59 {
60     QString serverName = "piper-vamp-simple-server";
61     float minimumVersion = 2.0;
62 
63     HelperExecPath hep(HelperExecPath::AllInstalled);
64 
65     auto servers = hep.getHelperExecutables(serverName);
66 
67     for (auto n: servers) {
68         SVDEBUG << "NOTE: PiperVampPluginFactory: Found server: "
69                 << n.executable << endl;
70         if (serverMeetsMinimumVersion(n, minimumVersion)) {
71             m_servers.push_back(n);
72         } else {
73             SVCERR << "WARNING: PiperVampPluginFactory: Server at "
74                    << n.executable
75                    << " does not meet minimum version requirement (version >= "
76                    << minimumVersion << ")" << endl;
77         }
78     }
79 
80     if (m_servers.empty()) {
81         SVDEBUG << "NOTE: No Piper Vamp servers found in installation;"
82                 << " the following paths are either absent or fail "
83                 << "minimum-version check:" << endl;
84         for (auto d: hep.getHelperCandidatePaths(serverName)) {
85             SVDEBUG << "NOTE: " << d << endl;
86         }
87     }
88 }
89 
~PiperVampPluginFactory()90 PiperVampPluginFactory::~PiperVampPluginFactory()
91 {
92     delete m_logger;
93 }
94 
95 bool
serverMeetsMinimumVersion(const HelperExecPath::HelperExec & server,float minimumVersion)96 PiperVampPluginFactory::serverMeetsMinimumVersion(const HelperExecPath::HelperExec &server,
97                                                   float minimumVersion)
98 {
99     QProcess process;
100     QString executable = server.executable;
101     process.setReadChannel(QProcess::StandardOutput);
102     process.setProcessChannelMode(QProcess::ForwardedErrorChannel);
103     process.start(executable, { "--version" });
104 
105     if (!process.waitForStarted()) {
106         QProcess::ProcessError err = process.error();
107         if (err == QProcess::FailedToStart) {
108             SVCERR << "WARNING: Unable to start server " << executable
109                    << " for version check" << endl;
110         } else if (err == QProcess::Crashed) {
111             SVCERR << "WARNING: Server " << executable
112                    << " crashed on version check" << endl;
113         } else {
114             SVCERR << "WARNING: Server " << executable
115                    << " failed on version check with error code "
116                    << err << endl;
117         }
118         return false;
119     }
120     process.waitForFinished();
121 
122     QByteArray output = process.readAllStandardOutput();
123     while (output.endsWith('\n') || output.endsWith('\r')) {
124         output.chop(1);
125     }
126 
127     QString outputString(output);
128     bool ok = false;
129     float version = outputString.toFloat(&ok);
130     if (!ok) {
131         SVCERR << "WARNING: Failed to convert server version response \""
132                << outputString << "\" into one- or two-part version number"
133                << endl;
134     }
135 
136     SVDEBUG << "Server " << executable << " reports version number "
137             << version << endl;
138 
139     float eps = 1e-6f;
140     return (version >= minimumVersion ||
141             fabsf(version - minimumVersion) < eps); // arf
142 }
143 
144 vector<QString>
getPluginIdentifiers(QString & errorMessage)145 PiperVampPluginFactory::getPluginIdentifiers(QString &errorMessage)
146 {
147     Profiler profiler("PiperVampPluginFactory::getPluginIdentifiers");
148 
149     QMutexLocker locker(&m_mutex);
150 
151     if (m_servers.empty()) {
152         errorMessage = QObject::tr("External plugin host executable does not appear to be installed");
153         return {};
154     }
155 
156     if (m_pluginData.empty()) {
157         populate(errorMessage);
158     }
159 
160     vector<QString> rv;
161 
162     for (const auto &d: m_pluginData) {
163         rv.push_back(QString("vamp:") + QString::fromStdString(d.second.pluginKey));
164     }
165 
166     return rv;
167 }
168 
169 Vamp::Plugin *
instantiatePlugin(QString identifier,sv_samplerate_t inputSampleRate)170 PiperVampPluginFactory::instantiatePlugin(QString identifier,
171                                           sv_samplerate_t inputSampleRate)
172 {
173     Profiler profiler("PiperVampPluginFactory::instantiatePlugin");
174 
175     if (m_origins.find(identifier) == m_origins.end()) {
176         SVCERR << "ERROR: No known server for identifier " << identifier << endl;
177         return nullptr;
178     }
179 
180     auto psd = getPluginStaticData(identifier);
181     if (psd.pluginKey == "") {
182         return nullptr;
183     }
184 
185     SVDEBUG << "PiperVampPluginFactory: Creating PiperAutoPlugin for server "
186         << m_origins[identifier] << ", identifier " << identifier << endl;
187 
188     auto ap = new piper_vamp::client::PiperAutoPlugin
189         (m_origins[identifier].toStdString(),
190          psd.pluginKey,
191          float(inputSampleRate),
192          0,
193          m_logger);
194 
195     if (!ap->isOK()) {
196         delete ap;
197         return nullptr;
198     }
199 
200     return ap;
201 }
202 
203 piper_vamp::PluginStaticData
getPluginStaticData(QString identifier)204 PiperVampPluginFactory::getPluginStaticData(QString identifier)
205 {
206     if (m_pluginData.find(identifier) != m_pluginData.end()) {
207         return m_pluginData[identifier];
208     } else {
209         return {};
210     }
211 }
212 
213 QString
getPluginCategory(QString identifier)214 PiperVampPluginFactory::getPluginCategory(QString identifier)
215 {
216     if (m_taxonomy.find(identifier) != m_taxonomy.end()) {
217         return m_taxonomy[identifier];
218     } else {
219         return {};
220     }
221 }
222 
223 QString
getPluginLibraryPath(QString identifier)224 PiperVampPluginFactory::getPluginLibraryPath(QString identifier)
225 {
226     // What we want to return here is the file path of the library in
227     // which the plugin was actually found -- we want to be paranoid
228     // about that and not just query
229     // Vamp::HostExt::PluginLoader::getLibraryPathForPlugin to return
230     // what the SDK thinks the likely location would be (in case our
231     // search order turns out to have been different)
232 
233     QStringList bits = identifier.split(':');
234     if (bits.size() > 1) {
235         QString soname = bits[bits.size() - 2];
236         auto i = m_libraries.find(soname);
237         if (i != m_libraries.end()) {
238             return i->second;
239         }
240     }
241     return QString();
242 }
243 
244 void
populate(QString & errorMessage)245 PiperVampPluginFactory::populate(QString &errorMessage)
246 {
247     QString someError;
248 
249     for (auto s: m_servers) {
250 
251         populateFrom(s, someError);
252 
253         if (someError != "" && errorMessage == "") {
254             errorMessage = someError;
255         }
256     }
257 }
258 
259 void
populateFrom(const HelperExecPath::HelperExec & server,QString & errorMessage)260 PiperVampPluginFactory::populateFrom(const HelperExecPath::HelperExec &server,
261                                      QString &errorMessage)
262 {
263     QString tag = server.tag;
264     string executable = server.executable.toStdString();
265 
266     PluginScan *scan = PluginScan::getInstance();
267     auto candidateLibraries =
268         scan->getCandidateLibrariesFor(PluginScan::VampPlugin);
269 
270     SVDEBUG << "PiperVampPluginFactory: Populating from " << executable << endl;
271     SVDEBUG << "INFO: Have " << candidateLibraries.size()
272             << " candidate Vamp plugin libraries from scanner" << endl;
273 
274     vector<string> from;
275     for (const auto &c: candidateLibraries) {
276         if (c.helperTag == tag) {
277             string soname = QFileInfo(c.libraryPath).baseName().toStdString();
278             SVDEBUG << "INFO: For tag \"" << tag << "\" giving library " << soname << endl;
279             from.push_back(soname);
280             QString qsoname = QString::fromStdString(soname);
281             if (m_libraries.find(qsoname) == m_libraries.end()) {
282                 m_libraries[qsoname] = c.libraryPath;
283             }
284         }
285     }
286 
287     if (from.empty()) {
288         SVDEBUG << "PiperVampPluginFactory: No candidate libraries for tag \""
289              << tag << "\"";
290         if (scan->scanSucceeded()) {
291             // we have to assume that they all failed to load (i.e. we
292             // exclude them all) rather than sending an empty list
293             // (which would mean no exclusions)
294             SVDEBUG << ", skipping" << endl;
295             return;
296         } else {
297             SVDEBUG << ", but it seems the scan failed, so bumbling on anyway" << endl;
298         }
299     }
300 
301     piper_vamp::client::ProcessQtTransport transport(executable, "capnp", m_logger);
302     if (!transport.isOK()) {
303         SVDEBUG << "PiperVampPluginFactory: Failed to start Piper process transport" << endl;
304         errorMessage = QObject::tr("Could not start external plugin host");
305         return;
306     }
307 
308     piper_vamp::client::CapnpRRClient client(&transport, m_logger);
309 
310     piper_vamp::ListRequest req;
311     req.from = from;
312 
313     piper_vamp::ListResponse resp;
314 
315     try {
316         resp = client.list(req);
317     } catch (const piper_vamp::client::ServerCrashed &) {
318         SVDEBUG << "PiperVampPluginFactory: Piper server crashed" << endl;
319         errorMessage = QObject::tr
320             ("External plugin host exited unexpectedly while listing plugins");
321         return;
322     } catch (const std::exception &e) {
323         SVDEBUG << "PiperVampPluginFactory: Exception caught: " << e.what() << endl;
324         errorMessage = QObject::tr("External plugin host invocation failed: %1")
325             .arg(e.what());
326         return;
327     }
328 
329     SVDEBUG << "PiperVampPluginFactory: server \"" << executable << "\" lists "
330             << resp.available.size() << " plugin(s)" << endl;
331 
332     for (const auto &pd: resp.available) {
333 
334         QString identifier =
335             QString("vamp:") + QString::fromStdString(pd.pluginKey);
336 
337         if (m_origins.find(identifier) != m_origins.end()) {
338             // have it already, from a higher-priority server
339             // (e.g. 64-bit instead of 32-bit)
340             continue;
341         }
342 
343         m_origins[identifier] = server.executable;
344 
345         m_pluginData[identifier] = pd;
346 
347         QStringList catlist;
348         for (const auto &cs: pd.category) {
349             catlist.push_back(QString::fromStdString(cs));
350         }
351 
352         m_taxonomy[identifier] = catlist.join(" > ");
353     }
354 }
355 
356 #endif
357